ThreadLocal 简介
作用
ThreadLocal 可以为每个线程存储自己的私有数据,做到数据隔离,防止自己的变量被其它线程篡改。
应用场景
- 当多个线程共享同一个资源且不需要同步时。解决多线程共享数据冲突的问题时,有两种思路,一种思路是加锁,另一种思路是每个线程自各创建共享资源的一个副本,互不干扰。ThreadLocal 不是在线程冲突时想办法解决冲突,而是避免了冲突,这样就避免了加锁导致的性能损耗,是一种用空间换时间的策略。如多个线程各自建立自己的数据库连接,避免竞争同一数据库连接引起的错误。
- 保存线程上下文信息,在线程内部任意需要的地方可以获取。
局限
不适用于多个线程需要同步的场景,无法解决共享对象的更新问题。如多个线程对同一个变量进行累加操作,因为一个线程的对变量的修改需要影响到其它线程,不然累加的结果就不对了。
最佳实践
ThreadLocal对象建议使用static修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象都可以操控这个变量。
把ThreadLocal定义为static还有一个好处,就是由于ThreadLocal有强引用在,那么在ThreadLocalMap里对应的Entry的键会永远存在,所以执行remove的时候就可以正确进行定位到并且删除。并且在无效Entry清除时,这个ThreadLocal对象对应的Entry不会被清理。
在不使用ThreadLocal的时候,主动调用remove方法进行清理,避免出现内存溢出情况。
public class ThreadLocalTest {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
try {
// do something with threadLocal
threadLocal.set(3);
threadLocal.get();
} finally {
threadLocal.remove();
}
}).start();
}
}
实现原理
每个Thread内部有一个ThreadLocalMap类型的成员属性threadLocals。ThreadLocalMap是一个普通类,内部持有一个Entry数组,数组长度默认是16,每个Entry是一个存储的线程内部数据。Entry继承了WeakReference,使用ThreadLocal作为key,也就是说通过弱引用指向ThreadLocal对象,另外Entry中持有一个Object类型的value。ThreadLocal表示value的类型,并作为key来从数组中定位Entry,value是实际存储的线程的内部隔离数据。
相关类有:Thread, ThreadLocalMap, Entry, ThreadLocal, Object(value)。
set
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocal调用set()方法时,先获取到当前线程,再获取到当前线程的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);
}
如果map为空,就创建map,在ThreadLocalMap的构造函数中,初始化Entry数组,根据ThreadLocal的hash方法算出在数组中的序号i,然后创建Entry,并添加在数组中序号为i的位置。
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) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
}
如果map不为空,就调用map的set()方法,在set方法中,先根据ThreadLocal的hash值算出在数组中的序列号i,获取i处Entry的值,然后分以下四种情况:
- Entry为空,直接创建Entry并添加在数组序号为i的位置。
- Entry不为空,且Entry的key和当前ThreadLocal对象相同,此时直接替换Entry的值。
- Entry不为空,且Entry的key为空,表示这个Entry是无效数据。此时先从当前i位置向前遍历,遇到序号为0的元素后遍历数组最后一个元素,直到遇到第一个Entry为null的位置为止。从新位置向后遍历,走过数组最后一个元素后到第一个元素,对每个Entry,如果key为ThreadLocal对象,替换value值,并向后检查所有每个Entry,如果Entry不为null且key为null,就把这些Entry从数组中清除。
- Entry不为空,且Entry的key不为空且不等于ThreadLocal对象,这时一直向后沿数组遍历,直到遇到上述三种情况停止。
get
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocal调用get方法时,先获取当前所在线程,并获取到线程所拥有的map。如果map不为空,根据ThreadLocal对象的hash值定位在数组中的位置i,取位置i处的Entry,如果Entry不为空且它的key等于ThreadLocal对象,返回这个Entry的值,如果Entry为空或key不等于ThreadLocal对象,向后遍历数组,当遇到Entry不为空且key不等于ThreadLocal对象时,继续向后遍历,当遇到key为空的Entry时,执行清理无效Entry操作,参考set部分,清理后返回ThreadLocal的默认值,当遇到Entry为空时,返回ThreadLocal的默认值。
内存泄露
引用关系
Thread --> ThreadLocalMap --> Entry --> value,这四者之间是强引用关系。
Entry通过弱引用指向ThreadLocal对象,另外ThreadLocal对象被创建的类强引用。
内存泄露什么时候发生
使用完ThreadLocal后,如果没有调用remove方法,在线程销毁之前,ThreadLocal对象对应的value可能会一直被Thread持有,这样就造成了内存泄露。当线程对象被gc回收后,就不会出现内存泄露,但是线程的生命周期都比较长,加上现在普遍使用的线程池,会让线程的生命更加长,不手动remove,value就可能不被释放,这样就造成了内存泄露。
为什么说线程销毁之前这段时间可能造成内存泄露,因为这和threadLocal对象的定义有关,也和ThreadLocalMap的无效Entry清理机制有关。如果threadLocal定义为类的静态属性,那么threadLocal的强引用就一直存在,此时无效Entry清理机制不会回收这个theadLocal对象对应的Entry,肯定会造成内存泄露。但如果theadLocal定义为局部变量,或者定义在类成员变量,但是这个类销毁了,或者threadLocal的对象被指向了null,此时这个threadLocal对象对应的Entry就变为了无效Entry,可能会被无效Entry清理机制清除,但不是一定会被清除,仍可能发生内存泄露,注意,这种情况下是可能,不是一定。
总结一下,在ThreadLocal不用之后,线程销毁之前,有两种情况会发生内存泄露:一、创建ThreadLocal的线程一直持有它的强引用,那么这个ThreadLocal对应的Entry对象中的value就会一直得不到回收,发生内存泄露;二、ThreadLocal已经没有强引用,发生GC的时候,它对应的Entry中的key也指向了null,但是ThreadLocalMap的get()方法碰巧一直固定访问几个一直存在的ThreadLocal,内存清理一直得不到执行,那么这此过期Entry和它们对应的value就发生了内存泄露。
无效Entry清理机制
当set()和get()方法执行时,遇到key为空的Entry时,会执行无效Entry清理。
清理时,先从当前位置向前遍历,直到第一个Entry为null的位置截止。再从这个新位置向后遍历,把key为null的Entry从数组中清除。
这种清理机制可以在一定程序上清除掉没有强引用指向的Entry,但是不能保证可以清除所有无用的threadLocal。一是如果threadLocal一直有强引用,即使不再使用,它们对应的Entry也不会被清除。二是,即使threadLocal没有强引用,对应Entry的key已经为null,但如果threadLocal一直固定存取几个已存在的Entry时,内存清楚机制也不会执行,也会有内存泄露
内存泄露怎么解决
因为无效Entry清理机制不能保证完全清除掉无效的Entry,也因为通常实现中threadLocal对象是静态定义的,强引用一直存在,此时无效Entry清理机制根本不会执行。
所以为解决内存泄露,应该在线程内,当不再使用ThreadLocal后,马上调用它的remove方法,手动把它从ThreadLocalMap中清除,避免内存泄露。
为什么Entry使用弱引用持有ThreadLocal
如果使用强引用持有ThreadLocal,那么当remove()没有执行的时候,无论创建ThreadLocal的地方是否还持有它的引用,因为Entry持有它的强引用,所以这个Entry将永远得不到释放,造成内存泄露。
而使用弱引用的话,当创建ThreadLocal的地方不再持有它的强引用时,系统中就只剩下Entry中持有它的弱引用,当GC发生时,ThreadLocal就会被回收,Entry的key就指向了null。随后当set(), get()等执行的时候,就有可能检查到key为null的Entry,并把它们回收,这样就避免了内存泄露。
另外一个原因是,通常线程的使用方式基本上都是线程池,所以线程的生命周期就很长,可能从你部署上线后一直存在,而 ThreadLocal 对象的生命周期可能没这么长,使用弱引用可以保证被销毁的ThreadLocal对应的Entry可以被清理掉。
为什么Entry不用弱引用持有value
因为如果用弱引用持有value,value会在GC时会清除,但如果这个Entry对应的ThreadLocal还存在的话,就会导致取到的value为空。
什么时候回收Entry
- 手动调用remove的时候
- Entry引用的key为空,且执行get(), set(), remove()方法的时候,会调用ThreadLocalMap的expungeStaleEntry方法,清除无效的Entry
- 线程销毁的时候
ThreadLocalMap 与 HashMap 的不同 hash 算法
java.util.HashMap使用链表法来处理冲突,每个冲突位置放置一个链表(红黑树)。
ThreadLocalMap,使用的是简单的线性探测法,先通过ThreadLocal的hash值在当前数组中定位一个序号i,如果发生了元素冲突,那么就向后遍历数组查找Entry,直到找到key相同或遇到Entry为null。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 根据threadLocal的哈希值,得到一个数组上对应的索引
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 进入循环表示该索引位置已被占用,发生了冲突
ThreadLocal<?> k = e.get();
// 如果key相同,直接重置即可
if (k == key) {
e.value = value;
return;
}
// 如果key为null,表示该entry已无效,从列表中清除原entry,并创建新entry放在这个位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果当前位置没有entry,表示没有被占,直接创建entry放在这个位置
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}