ThreadLocal是什么?
ThreadLocal是JDK原生为我们提供的可保存线程隔离的变量的容器。
本身ThreadLocal变量是保存在Thread类的ThreadLocalMap类型的成员变量中的,且ThreadLocal变量本身作为ThreadLocalMap的key(这里可能很多人有误区,我见过很多人认为是ThreadLocal类内部保存了一个map)
下面给出Thread、ThreadLocal、ThreadLocalMap三者之间的引用关系图
这个图很关键,掌握了这个图关于ThreadLocal的引用问题就已经完全掌握了
在许多中间件以及框架中都有应用,比如Spring Tx中就是通过ThreadLocal来保存事务相关的信息。
可以自行查看
TransactionSynchronizationManager,这里不再赘述。
有些同学可能纳闷在某些情况下ThreadLocal的作用是否可以通过方法变量入参来实现,确实可以,但是有多个缺陷
- 需要在整个方法调用链中传递这个变量
- 整体的调用链路由自己一个人把控,否则就要要求别人接口设计多带一个入参
ThreadLocal源码
ThreadLocal的源码其实很简单明了
主要涉及ThreadLocal、ThreadLocalMap、Thread三个类
了解ThreadLocal源码,一般只需要关注三个关键方法即set、get、remove
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);
}
}
可以看到主要分三步
- 获取当前线程
- 获取当前线程的成员变量threadLocals (ThreadLocalMap类型)
- 通过操作ThreadLocalMap的set方法进行值的设置/初始化ThreadLocalMap
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();
}
get方法和set方法流程几乎一致
- 获取当前线程
- 获取当前线程的成员变量threadLocals (ThreadLocalMap类型)
- 通过ThreadLocalMap获取值/设置并返回默认值
remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
- 获取当前线程
- 获取当前线程的成员变量threadLocals (ThreadLocalMap类型)
- 调用ThreadLocalMap的remove方法
ThreadLocalMap
通过上述ThreadLocal中的源码可以知道
其实操作的核心是threadLocals线程成员变量,即ThreadLocalMap类的相关方法
ThreadLocalMap作为ThreadLocal类的静态内部类,使用Entry[] table数组实现了Map的相关操作
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继承了WearRefrence类,将key作为弱引用存在,其value就是我们通过ThreadLocal.set()方法set进来的值,为强引用。
set方法
本文只重点介绍set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 上面就是通过hash值以及位运算计算下标位置
// 因为ThreadLocal是通过开放寻址法来消除hash冲突
// 所以会一直遍历到entry为null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果key已存在 覆盖value
if (k == key) {
e.value = value;
return;
}
// 遍历到key由于弱引用被回收 也会创建entry
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 找到entry为null的下标 创建entry
tab[i] = new Entry(key, value);
int sz = ++size;
// 后续清理key被回收的entry
// 以及判断是否需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocal的设计与可能的坑
ThreadLocalMap为何将key设计为弱引用
首先先回顾下java的四种引用
- 强引用:一般没有特别申明的对象都是强引用。这种对象只有在GCroots找不到它的时候才会被回收。
- 软引用(SoftReference的子类):GC后内存不足的情况将只有这种引用的对象回收。
- 弱引用(WeakReference的子类): GC时回收只有此引用的对象(无论内存是否不足)。
- 虚引用(PhantomReference子类):没有特别的功能,类似一个追踪符,配合引用队列来记录对象何时被回收 为什么设计为弱引用,网上一般说法是为了避免内存泄漏 我们来看看怎么避免的内存泄漏
弱引用可以斩断entry->ThreadLocal这一条引用链,从而可以在ThreadLocalRef=null的时候,将ThreadLocal实例回收掉。 如果没有设置弱引用的话,ThreadLocal实例就有可能导致内存泄漏,因为获取不到他了,remove都没法执行 这里额外提一嘴,一般规范ThreadLocal变量是作为static final类型的,所以即使斩断这一条引用链,ThreadLocal实例也不会被回收
一个问题,为什么value不能设置为弱引用?
因为value只有一条entry->Value的引用链,如果value弱引用,值可能被提前回收;我们就有可能获取不到值了,ThreadLocal存在的意义也就没了
而key还存在ThreadLocalRef这一个引用链,所以如果不是手动赋值ThreadLocalRef=null,是不会被回收的
ThreadLocal内存泄漏
在网上一直流传着ThreadlLocal的不规范使用会造成内存泄漏问题,让我们来分析下问题来源 通过上面的引用图我们可以知道,entry.Value会被强引用,也就是只有他才会造成内存泄漏 但其实条件还蛮苛刻的
- 首先要有线程池这种线程不会销毁的场景
- 没有调用set/get/remove方法
其实ThreadLocal内部虽然有惰性清理方法
expungeStaleEntry,但还是建议在使用完后主动调用remove方法
线程重用数据混乱
在线程池场景下,如果没有做好清理工作,可能会导致下一个任务获取数据错乱
解决方法依然是注意在使用完成后调用remove方法
综上所述,其实只需要注意显示清理ThreadLocal,即调用其remove方法即可避免落入ThreadLocal的坑里