阅读 Eureka 的 StringCache 源码 get 到的知识

今天在读 Eureka 源码时,看到了它里边实现了一个工具类 StringCache 阅读后我产生了几个疑问,查阅资料后一一进行了解决,受益良多并以此文进行记录。

StringCache 实现了一个字符串缓存,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public class StringCache {

public static final int LENGTH_LIMIT = 38;

private static final StringCache INSTANCE = new StringCache();

private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Map<String, WeakReference<String>> cache = new WeakHashMap<String, WeakReference<String>>();
private final int lengthLimit;

public StringCache() {
this(LENGTH_LIMIT);
}

public StringCache(int lengthLimit) {
this.lengthLimit = lengthLimit;
}

public String cachedValueOf(final String str) {
if (str != null && (lengthLimit < 0 || str.length() <= lengthLimit)) {
// Return value from cache if available
try {
lock.readLock().lock();
WeakReference<String> ref = cache.get(str);
if (ref != null) {
return ref.get();
}
} finally {
lock.readLock().unlock();
}

// Update cache with new content
try {
lock.writeLock().lock();
WeakReference<String> ref = cache.get(str);
if (ref != null) {
return ref.get();
}
cache.put(str, new WeakReference<>(str));
} finally {
lock.writeLock().unlock();
}
return str;
}
return str;
}

public int size() {
try {
lock.readLock().lock();
return cache.size();
} finally {
lock.readLock().unlock();
}
}

public static String intern(String original) {
return INSTANCE.cachedValueOf(original);
}
}

什么是字符串常量池?

1
2
3
String s = "a" + "bc";
String t = "ab" + "c";
System.out.println(s == t);

上边这段程序会打印 true(尽管我们没有使用正确比较字符串的 equals 方法)

当编译器优化字符串的字面值时,它看到 st 有相同的值,因为字符串在 Java 中是不可变的,所以提供同一个字符串对象也是安全的,因此 st 指向了同一个对象并且节省了一丢丢的内存。

「字符串常量池」的灵感来源于这样的想法:所有已定义的字符串都存储在一个「池子」中,在创建新的 String 对象前,编译器需要检查这个字符串是否已经被定义,若已经在「池子」中存在就直接拿出来用。

也就是说 Java 编译器已经用字符串常量池实现了字符串缓存的特性,在我们直接使用双引号来声明 String 对象时会自动利用以上特性,如果不是用双引号声明的,可以用 String 提供的 intern() 方法。intern() 方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

示例程序:

1
2
3
4
5
6
7
8
9
10
String a1 = "aaa";
String a2 = "aaa";
String a3 = new String("aaa");
System.out.println(a1 == a2); // true
System.out.println(a1 == a3); // false
System.out.println(a1 == a3.intern()); // true

String b1 = new String("bbb").intern();
String b2 = "bbb";
System.out.println(b1 == b2); // true

为什么 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,取值时先获取锁,如果存在就直接从 Mapget 出来然后返回,不存在就 put 进去作为缓存以便下次使用。

这里声明的 Map 类型是 WeakHashMap,这种 Map 的特点是,当除了自身有对 key 的引用外,此 key 没有其他引用那么这个 map 会自动丢弃此值。

示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
String a = new String("a");
String b = new String("b");
Map<String, String> weakmap = new WeakHashMap();
Map<String, String> map = new HashMap();
map.put(a, "aaa");
map.put(b, "bbb");

weakmap.put(a, "aaa");
weakmap.put(b, "bbb");

map.remove(a);
a = null;
b = null;

System.gc();
Iterator i = map.entrySet().iterator();
while (i.hasNext()) {
Map.Entry en = (Map.Entry)i.next();
System.out.println("map: "+en.getKey()+":"+en.getValue());
}

Iterator j = weakmap.entrySet().iterator();
while (j.hasNext()) {
Map.Entry en = (Map.Entry)j.next();
System.out.println("weakmap: "+en.getKey()+":"+en.getValue());
}

我们声明了两个 Map 对象,一个是 HashMap,一个是 WeakHashMap,同时向两个 map 中放入 ab 两个对象,从 HashMapremovea 并且将 ab 都指向 null 时,WeakHashMap 中的 a 将自动被回收掉。出现这个状况的原因是,对于 a 对象而言,当从 HashMapremovea 并且将 a 指向 null 后,除了 WeakHashMap 中还保存 a 外已经没有指向 a 的指针了,所以 WeakHashMap 会自动舍弃掉 a,而对于 b 对象虽然指向了null,但 HashMap 中还有指向 b 的指针,所以 WeakHashMap 将会保留 b

以上程序得到的结果是:

1
2
map: b:bbb
weakmap: b:bbb

WeakReference 和普通的引用有什么区别?

可以看到我们的 StringCache 中的 Map 值类型用的是 WeakReference<String>,如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference

如果不这样用,会导致我们 Map 的值也会引用我们想缓存的字符串,这就导致即使 key 已经没有任何地方引用了,这个 WeakHashMap 也不会丢弃此值。

ReentrantReadWriteLock 有什么特性?

ReentrantReadWriteLock 是一个读写锁:分为读锁和写锁,多个读锁不互斥,读锁与写锁互斥,这是由 jvm 自己控制的,你只要上好相应的锁即可。如果你的代码只读数据,可以很多人同时读,但不能同时写,那就上读锁;如果你的代码修改数据,只能有一个人在写,且不能同时读取,那就上写锁。总之,读的时候上读锁,写的时候上写锁。

线程进入读锁的前提条件:

  • 没有其他线程的写锁,
  • 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个

线程进入写锁的前提条件:

  • 没有其他线程的读锁
  • 没有其他线程的写锁