今天在读 Eureka 源码时,看到了它里边实现了一个工具类 StringCache
阅读后我产生了几个疑问,查阅资料后一一进行了解决,受益良多并以此文进行记录。
StringCache
实现了一个字符串缓存,代码如下
1 | public class StringCache { |
什么是字符串常量池?
1 | String s = "a" + "bc"; |
上边这段程序会打印 true
(尽管我们没有使用正确比较字符串的 equals
方法)
当编译器优化字符串的字面值时,它看到 s
和 t
有相同的值,因为字符串在 Java 中是不可变的,所以提供同一个字符串对象也是安全的,因此 s
和 t
指向了同一个对象并且节省了一丢丢的内存。
「字符串常量池」的灵感来源于这样的想法:所有已定义的字符串都存储在一个「池子」中,在创建新的 String
对象前,编译器需要检查这个字符串是否已经被定义,若已经在「池子」中存在就直接拿出来用。
也就是说 Java 编译器已经用字符串常量池实现了字符串缓存的特性,在我们直接使用双引号来声明 String
对象时会自动利用以上特性,如果不是用双引号声明的,可以用 String
提供的 intern()
方法。intern()
方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。
示例程序:
1 | String a1 = "aaa"; |
为什么 Eureka 要再造轮子?
既然 Java 编译器已经对相同的字符串进行了优化,为什么 Eureka 还要再造一个轮子呢,因为字符串常量池在存储大量的字符串后,会出现严重的性能问题。
以下解释来自美团点评技术团队编写的 深入解析String#intern 一文:
Java 使用 JNI 调用 C++ 实现的 StringTable 的 intern 方法,StringTable 的 intern 方法跟 Java 中的 HashMap 的实现是差不多的,只是不能自动扩容。默认大小是1009。
要注意的是,String 的 String Pool 是一个固定大小的 Hashtable,默认值大小长度是1009,如果放进 String Pool 的 String 非常多,就会造成 Hash 冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用 String.intern 时性能会大幅下降(因为要一个一个找)。
好了原因解释清楚了,我们来看一下具体实现中有那哪些问题。
WeakHashMap 和 HashMap 有什么区别?
StringCache
的代码不难理解,大致就是声明一个锁和一个 Map
,取值时先获取锁,如果存在就直接从 Map
中 get
出来然后返回,不存在就 put
进去作为缓存以便下次使用。
这里声明的 Map
类型是 WeakHashMap
,这种 Map
的特点是,当除了自身有对 key
的引用外,此 key
没有其他引用那么这个 map
会自动丢弃此值。
示例程序:
1 | String a = new String("a"); |
我们声明了两个 Map
对象,一个是 HashMap
,一个是 WeakHashMap
,同时向两个 map
中放入 a
、b
两个对象,从 HashMap
中 remove
掉 a
并且将 a
、b
都指向 null
时,WeakHashMap
中的 a
将自动被回收掉。出现这个状况的原因是,对于 a
对象而言,当从 HashMap
中 remove
掉 a
并且将 a
指向 null
后,除了 WeakHashMap
中还保存 a
外已经没有指向 a
的指针了,所以 WeakHashMap
会自动舍弃掉 a
,而对于 b
对象虽然指向了null
,但 HashMap
中还有指向 b
的指针,所以 WeakHashMap
将会保留 b
。
以上程序得到的结果是:
1 | map: b:bbb |
WeakReference 和普通的引用有什么区别?
可以看到我们的 StringCache
中的 Map
值类型用的是 WeakReference<String>
,如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference。
如果不这样用,会导致我们 Map
的值也会引用我们想缓存的字符串,这就导致即使 key
已经没有任何地方引用了,这个 WeakHashMap
也不会丢弃此值。
ReentrantReadWriteLock 有什么特性?
ReentrantReadWriteLock
是一个读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。
线程进入读锁的前提条件:
- 没有其他线程的写锁,
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁