ThreadLocal 分析
从功能上来说,它类似一个全局的Map,key是线程。不同线程get时拿到的都是专属于自己的那个对象,互相隔离,完全不存在并发问题。
典型的使用方式
// 摘自 j.u.c.ThreadLocalRandom
private static final ThreadLocal<ThreadLocalRandom> localRandom = // ThreadLocal对象都是static的,全局共享
new ThreadLocal<ThreadLocalRandom>() { // 初始值
protected ThreadLocalRandom initialValue() {
return new ThreadLocalRandom();
}
};
localRandom.get(); // 拿当前线程对应的对象
localRandom.put(...); // put
典型使用场景
- 用空间换并发度;
- 在线程范围内传参,如 hibernate 的 session;
实现
一个非常自然想法是用一个线程安全的 Map<Thread,Object>
实现:
class ThreadLocal {
private Map values = Collections.synchronizedMap(new HashMap());
public Object get() {
Thread curThread = Thread.currentThread();
Object o = values.get(curThread);
if (o == null && !values.containsKey(curThread)) {
o = initialValue();
values.put(curThread, o);
}
return o;
}
public void set(Object newValue) {
values.put(Thread.currentThread(), newValue);
}
}
但这是非常naive的:
ThreadLocal
本意是避免并发,用一个全局Map显然违背了这一初衷;- 用
Thread
当key,除非手动调用remove
,否则即使线程退出了 1)该Thread
对象无法回收; 2)该线程在所有ThreadLocal
中对应的value也无法回收。
JDK 的实现刚好是反过来的:
每个Thread对象内都存在一个ThreadLocal.ThreadLocalMap
对象,保存着该线程所有用到的ThreadLocal
及其value。
ThreadLocalMap
是定义在ThreadLocal
类内部的私有类,它是采用“开放定址法”解决冲突的hashmap。key是ThreadLocal
对象。当调用某个ThreadLocal
对象的get
或put
方法时,首先会从当前线程中取出ThreadLocalMap,然后查找对应的value:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //拿到当前线程的ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 以该ThreadLocal对象为key取value
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
现在来看看它的哈希策略。所有ThreadLocal
对象共享一个AtomicInteger
对象nextHashCode
用于计算hashcode,一个新对象产生时它的hashcode就确定了,算法是从0开始,以HASH_INCREMENT = 0x61c88647
为间隔递增,这是ThreadLocal
唯一需要同步的地方。根据hashcode定位桶的算法是将其与数组长度-1进行与操作:key.threadLocalHashCode & (table.length - 1)
。
0x61c88647
这个魔数是怎么确定的呢?
ThreadLocalMap
的初始长度为16,每次扩容都增长为原来的2倍,即它的长度始终是2的n次方,上述算法中使用0x61c88647
可以让hash的结果在2的n次方内尽可能均匀分布,减少冲突的概率。具体原因我也不知道,不过这是一个好的参考。
内存管理
ThreadLocalMap.Entry
继承自WeakReference
,是 key(ThreadLocal 对象) 和 value 的容器:
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
一旦某个ThreadLocal
对象没有强引用了,它在所有线程内部的ThreadLocalMap
中的key都将被GC掉(此时value还未回收),在map后续的get/set
中会探测到key被回收的entry,将其 value 设置为 null 以帮助GC,因此 value 在 key 被 GC 后可能还会存活一段时间,但最终也会被回收。这个过程和java.util.WeakHashMap
的实现几乎是一样的。
一旦线程退出,Thread
对象被回收了,内部ThreadLocalMap
中的value也可以被回收了。但是线程池的场景需要注意,线程池中的线程可能永远不会退出,只会阻塞,如果队列中的某个任务向ThreadLocal
put了一个对象,但任务结束后并未清空,那么这个对象在ThreadLocal
的下一次put或clear前永远不会被GC;这种情况下,假如线程池有200个线程,那么一个ThreadLocal
最多可能造成200个对象的内存泄露。