解读ThreadLocal

简单使用
class ThreadLocalTest {
companion object {
/**
* 定义一个ThreadLocal变量
*/
private val threadLocal = object : ThreadLocal<Int>() {
/**
* 重写该方法,指定一个初始值
*/
override fun initialValue(): Int {
return 1
}
}
@JvmStatic
fun main(args: Array<String>) {
/**
* 开启子线程
*/
thread(name = "threadLocal-1") {
// 先获取旧值
val oldValue = threadLocal.get()
// 设置新值
threadLocal.set(3)
// 再获取新值
val newValue = threadLocal.get()
// 打印线程名称,旧值,新值
println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue")
}
/**
* 开启子线程
*/
thread(name = "threadLocal-2") {
// 先获取旧值
val oldValue = threadLocal.get()
// 设置新值
threadLocal.set(8)
// 再获取新值
val newValue = threadLocal.get()
// 打印线程名称,旧值,新值
println("thread name :${Thread.currentThread().name}| oldValue = $oldValue, newValue = $newValue")
}
}
}
}
运行结果

实现原理
类图

从图中我们可以看到,在我们的测试类
ThreadLocalTest中,开启了两个线程,thread-1和thread-2,分别在两个线程中调用我们创建好的threadLocal变量的set和get方法,先分别看下set和get的源码
set方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap成员变量
ThreadLocalMap map = getMap(t);
if (map != null)
// 如果能获取到,则直接设置值
map.set(this, value);
else
// 否则创建ThreadLocalMap对象再赋值
createMap(t, value);
}
get方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取当前线程的ThreadLocalMap成员变量
ThreadLocalMap map = getMap(t);
if (map != null) {
// 获取map对象中对应的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
// 取出key对应的值并返回
T result = (T)e.value;
return result;
}
}
// 否则创建ThreadLocalMap对象并返回initialValue()方法的值
return setInitialValue();
}
getMap()
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
createMap()
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从
set方法的源码中我们可以看到,首先会获取到调用set方法时程序所在的线程,然后再拿到当前线程对应的ThreadLocalMap成员变量,如果不为空,再调用ThreadLocalMap成员变量对应的set方法,把当前ThreadLocal对象作为key,保存到ThreadLocalMap对象中,为空,则创建再保存相应的值,ThreadLocalMap对象会持有一个Entry类型的数组,Entry是一个类似于Map结构的类,继承于WeakReference,get方法也有类似的逻辑,不做详细分析。
结论
不难发现,我们在调用
ThreadLocal的set或者get方法时,内部实际上调用的是ThreadLocalMap的set或者get方法,我们可以理解为ThreadLocal只是对set或者get方法做了一层封装,而ThreadLocal本身作为一个壳供外部使用,而ThreadLocalMap是线程持有的成员变量,因此,我们在多线程的场景中使用ThreadLocal来保存变量,实际上就是不同的线程的成员变量ThreadLocalMap对变量做了一个副本,对变量的值的改变也只是改变了当前线程的变量的值,不会影响到其他线程的变量的值的改变,起到了一个线程隔离的作用。
关系图

简单解释下,
ThreadLocalMap是Thread中的成员变量,ThreadLocalMap中持有Entry对象,Entry是一个类似于Map的数据结构的类,ThreadLocal作为key,Object作为值被保存下来,Entry是一个数组,目的是为了在同一个线程中能够保存多种数据类型的变量副本。
内存泄漏问题
为什么会导致内存泄漏
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread->ThreaLocalMap->Entry->value,永远无法回收,造成内存泄漏。 其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。 但是这些被动的预防措施并不能保证不会内存泄漏
为什么ThreadLocal设计成弱引用
在源码当中,
ThreadLocal被设计成了弱引用,目的是降低OOM的可能性,不会因此避免OOM,我们都知道,弱引用在JVM中,当系统进行一次GC时,会回收掉被标记为弱引用的对象,ThreadLocalMap的生命周期是跟Thread的生命周期一致的,如果Thread的生命周期足够长,Thread会一直持有ThreadLocalMap对象:
- 如果是强引用,
ThreadLocalMap也会一直持有ThreadLocal,作为key的ThreadLocal也就会随着ThreadLocalMap的生命周期一直存在,得不到释放,慢慢累积,key会越积越多,最终导致内存泄漏。- 如果是弱引用,
GC时,当对ThreadLocal的强引用被回收时,系统会自动回收掉这部分ThreadLocal,并且当我们在下一次调用set、get或者remove方法时,也会去清除掉这部分ThreadLcoal,从而降低对内存的消耗,但是ThreadLocalMap依然还持有对Entry的引用,而Entry对value也是强引用,因此如果不手动释放,也会造成内存泄漏
如何避免
既然知道了造成内存泄漏的原因,那我们也就能够对症下药了,就是在每次使用完
ThreadLocal的时候调用remove()方法清除掉value就行了。
转载自:https://juejin.cn/post/7232524757526446140