ThreadLocal 是一个线程内共享数据的类,其原理是在线程有一个 ThreadLocalMap
,key是ThreadLocal对象,value是自定义的数据,所以在同一个线程中,用同一个threadlocal去get数据,能取到同样的数据。实现线程内数据共享。
ThreadLocalMap
说白了 ThreadLocalMap 就是有一个 Entry 的数组, Entry就是一个有着两个成员变量的类。一个变量是 value
, 另一个成员变量 在继承的 WeakReference
里面,是 referent
,当GC的时候这个成员变量就会被释放
threadLocal 用法很简单,就是set 和get不做过多赘述。我们来看看为什么ThreadLocalMap的key要做成 WeakReference
.
我们先来看如下的引用关系:
如果entry的那个弱引用改成强引用的话,那么在很多情况下, 线程的生命周期和程序是相同的,那么就算用户 threadLocal = null; 想要释放threadLocal的内存地址,也没用。造成内存泄漏。所以设计成弱引用的时候,当用户想要释放threadLocal 资源的时候(虽然很少) threadLocal = null; 线程的弱引用也会在下次GC的时候回收。
那么问题来了,为什么value不也设计成弱引用呢?
因为最根本的原因是因为 key 和 value 的生命周期是不一样的。 key 和 value是两个代码创建出来的,他们并不能保证一起消失,所以当 value如果设计成弱引用的时候,可能value指向消失了,但是key的强引用还存在,那么就可能出现key存在但是get不到东西的情况。
内存泄漏问题
上面说了,为了保证key一定能get到value的情况。在线程中强引用了value,那么这个value的生命周期就和该线程相同了。造成了内存泄漏。 就算在ThreadLocalMap里面的添加方法的时候会去删除key为空的value
条件是
- hash冲突会判空删除
- 插入的下标往后遍历查找为空的数据删除
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
# 在hash冲突的时候, 如果上一个key为空,就把value给删了
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
# 插入当前的地址往后遍历一波,查看有没有value为空的数据
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
即使是有这样的判断,如果key在数据之前,并且没有hash碰撞,那么value也永远都删除不掉。
所以在用完value之后一定要主动调用remove让线程删除引用
, remove方法最终调用如下
数据错误问题
有的人可能觉得,自己的threadLocal是静态变量,生命周期本来就跟项目相同,并且一个项目中线程几十个存放的也都是一些用户数据,没有太多的泄漏。那么ThreadLocal就还会因此引发第二个问题,数据错误。
现在项目中大部分都采用线程池技术,Tomcat,Apache 等等。
如果在这一次请求中存入了用户的数据没有释放,在其他用户访问的时候可能复用了当前线程,然后取出来的还是上一个用户的数据
ThreadLocal 线程共享
Thread 类里面还有一个成员变量 inheritableThreadLocals
可以用来做父子线程的数据共享,原理很简单,就是在创建线程的时候判断父线程的这个变量有没有值,如果有值,则也拷贝一份到自身。