类,它没有直接使用 HashMap,而是一个自定义的哈希表,使用数组实现,数组元素是 Entry
。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
类继承了 WeakReference<ThreadLocal<?>>
,我们可以把它看成是一个键值对。键是当前的 ThreadLocal
对象,值是存储的对象。注意 ThreadLocal 对象的引用是弱引用,值对象 value 的引用是强引用。ThreadLocal 使用弱引用其实很好理解,源码注释中也告诉了我们答案:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys
Thread
持有 ThreadLocalMap
的强引用,ThreadLocalMap
中的 Entry
的键是 ThreadLocal
引用。如果线程长期存活或者使用了线程池,而 ThreadLocal
在外部又没有任何强引用了,这种情况下如果 ThreadLocalMap
的键仍然使用强引用 ThreadLocal
,就会导致 ThreadLocal 永远无法被垃圾回收,造成内存泄露。
图片来源:https://www.jianshu.com/p/a1cd61fa22da
那么,使用弱引用是不是就万无一失了呢?答案也是否定的。同样是上面说到使用情况,线程长期存活,由于 Entry 的 key 使用了弱引用,当 ThreadLocal 不存在外部强引用时,可以在 GC 中被回收。但是根据可达性分析算法,仍然存在着这么一个引用链:
Current Thread -> ThreadLocalMap -> Entry -> value
key 已经被回收了,此时 key == null
。那么,value
呢?如果线程长期存在,这个针对 value 的强引用也会一直存在,外部是否对 value 指向的对象还存在其他强引用也不得而知。所以这里还是有几率发生内存泄漏的。就算我们不知道外部的引用情况,但至少在这里应该是可以切断 value
引用的。
所以,为了解决可能存在的内存泄露问题,我们有必要对于这种 key 已经被 GC 的过期 Entry 进行处理,手动释放 value 引用。当然,JDK 中已经为我们处理了,而且处理的十分巧妙。下面就来看看 ThreadLocalMap
的源码。
构造函数
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
table
是存储 Entry 的数组,初始容量 INITIAL_CAPACITY
是 16。
firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
是 ThreadLocalMap 计算哈希的方式。&(2^n-1)
其实等同于 % 2^n
,位运算效率更高。
threadLocalHashCode
是如何计算的呢?看下面的代码:
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private final int threadLocalHashCode = nextHashCode();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
0x61c88647
是一个增量,每次取哈希都要再加上这个数字。又是一个神奇的数字,让我想到了 Integer
源码中的 52429
这个数字,见 走进 JDK 之 Integer 。0x61c88647
背后肯定也有它的数学原理,总之肯定是为了效率。
原理就不去探究了,其实我也不知道是啥原理。不过我们可以试用一下,看看效果如何。按照上面的方式来计算连续几个元素的哈希值,也就是在 Entry 数组中的位置。代码如下:
public class Test {
private static final int INITIAL_CAPACITY = 16;
private static final int HASH_INCREMENT = 0x61c88647;
private static AtomicInteger nextHashCode = new AtomicInteger();
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
private static int hash() {
return nextHashCode() & (INITIAL_CAPACITY - 1);
}
public static void main(String[] args) {
for (int i = 0; i < 8; i++) {
System.out.println(hash());
}
}
}
运算结果如下:
0
7
14
5
12
3
10
1
计算结果分布还是比较均匀的。既然是哈希表,肯定就会存在哈希冲突的情况。那么,ThreadLocalMap 是如何解决哈希冲突呢?很简单,看一下 nextIndex()
方法。
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
在不超过 len
的情况下直接加 1,否则置 0。其实这样又可以看成一个环形数组。
接下来看看 ThreadLocalMap 的数据是如何存储的。
set()
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);