1.作用
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。
可以总结为一句话:ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
至于ThreadLocal为什么可以解决多线程程序并发问题,多线程程序之所以会有安全问题,是因为多个线程都对共享变量进行了一些操作,而有一些操作需要同步才能保证其正确性。而ThreadLocal做的就是把这个变量变成不是线程之间共享,就不会导致安全性问题。ThreadLocal是用来保证线程内共享变量。
2.数据结构
ThreadLocal的类图
我们可以看到ThreadLocal内中的详情,我们可以看到ThreadLocal类中有一个静态类——ThreadLocalMap。这个静态类中也有一个Entry的静态类,ThreadLocalMap与Map中的结构类似,key是ThreadLocal,value是object类型。
Thread的类图
我们看到Thread类中就有一个属性就是ThreadLocalMap的对象。
在了解完这两个类之后我们可以看得懂下面的结构图了。
画的有点乱,大家不要在意这些细节。Thread类中有一个属性是ThreadLocalMap用来存储ThreadLocal的键值对,每有一个变量来的时候,我们想要每个线程中都有这样一个变量,例如Hibernate的中将Session绑定到每个线程中,我们声明一个session(ThreadLocal的对象),调用ThreadLocal的方法针对每个线程生成不同value的值,保存在每个线程自己的一个ThreadLocalMap对象中,就相当于每个线程都有一个session变量,但是每个线程的值都是独立的,没有任何关系。就生成了每个线程自己的变量值。ThreadLocal对象相当于变量名,而每个键值对中的value相当于变量值,而ThreadLocalMap相当于管理线程变量的数组。
3.主要使用的方法
ThreadLocal中方法的主要的思路,先得到当前线程,然后再得到map,之后进行操作。
在每个线程内使用ThreadLocal对象(封装了一些操作,会比直接操作ThreadLocalMap简单)。
ThreadLocal类的核心方法
(1)get()方法用于获取当前线程的副本变量值。
(2)set()方法用于保存当前线程的副本变量值。
(3)initialValue()为当前线程初始副本变量值。
(4)remove()方法移除当前前程的副本变量值。
深入源码:
(1)get方法
public T get() {
// 1.得到当前线程
Thread t = Thread.currentThread();
// 2.通过调用getMap方法得到当前线程内对应的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
//3.判断map是否空
if (map != null) {
//如果不为空,则从ThreadLocalMap对象中取出线程对应的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
// 如果map为空或者取出的value为空,则初始化变量值,并返回
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 设置键值对的value的初始值为null
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
(2)set方法
public void set(T value) {
//1.得到当前的线程
Thread t = Thread.currentThread();
//2.通过线程得到map对象
ThreadLocalMap map = getMap(t);
//3.判断map是否为空
if (map != null)
//map不为空则存入value
map.set(this, value);
else
//map为空则先创建并存入
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
(3)remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
4.ThreadLocalMap的问题
(1)Hash冲突导致效率低
和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。显然ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。
建议:每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。(或者自己重写解决hash冲突的方式?)
(2)内存泄露
ThreadLocal在ThreadLocalMap中是以一个弱引用的身份被Entry的key引用的,因此如果ThreadLocal没有外部引用来引用它,那么ThreadLocal会在JVM垃圾收集时被回收。这时候就会出现Entry的key已经被回收,出现一个null的key的情况,外部读取ThreadLocalMap的元素是无法通过null的key来找到value的。因此如果此前线程的生命周期很长,一直存在,那么内部的ThreadLocalMap对象也一直存在。这些null key就存在一条强引用链的关系一直存在:Thread->ThreadLocalMap->Entry->value,这条强引用链会导致Entry不会回收,value也不会回收,但是Entry的key的引用却已经被回收的情况,导致内存泄露。
但是JVM团队已经考虑到了这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄露:在ThreadLocal的get()、set()、remove()方法调用的时候会清理线程ThreadLocalMap的所有的Entry的key为null的value,并将Entry设置为null,方便下次内存回收。
这是ThreadLocalMap中擦除key为null的节点代码,防止内存泄露。将当前Entry删除后,会继续循环往下检查是否有key为null的节点,如果有则一并删除,防止内存泄漏。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 将当前结点置空
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
// 继续循环往下检查是否有key为null的节点,如果有则一并删除,防止内存泄漏
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。
有人觉得ThreadLocal发生内存泄露是因为弱引用,其实并不是。
我们可以分情况讨论一下:
(1)key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。(包括key和value)
(2)key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。但如果没有调用就会发生泄露,key使用弱引用只是能够保证key不发生泄露,但是value会发生泄露。
ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。