ThreadLocal那些事
每次在需要的时候都会去看源码,但是一直没有整理出来,这次借着这个机会整理一下。本文不会记录所有的源码内容分析,因为这种文章在网上随便都能搜到,不想复制粘贴。这里只想记录以下几个问题:
- 在什么场景下需要使用ThreadLocal
- ThreadLocal 与 Thread 的关联关系以及面试中经常被问到的点
- 内存泄漏的问题
一、在什么场景下需要使用ThreadLocal
在开发中我们肯定会遇到并发的场景,多个线程争抢更新同一个变量。这个时候我们可以选择采用锁的方式对变量进行控制,但是处理并发问题呢,其实还有另外一种方式。就是ThreadLocal。这里说是另外一种方式,就是说在处理并发问题上的处理思路与锁不同,ThreadLocal不是解决线程同步的问题,它是通过线程隔离来实现线程安全,每个线程保存一份自己的变量副本,这样就不会产生冲突 。
二、ThreadLocal 与 Thread
ThreadLocal通过名字我们就可以理解,意思是线程本地变量,那就是每个线程拥有自己的,自然也就不会与其他线程产生通过并发的问题,也就没有数据一致性的问题了。那么JDK是怎么实现的呢?下面来记录几个比较重要的点。
首先是ThreadLocal这个类,里面有一个内部类叫做ThreadLocalMap
static class ThreadLocalMap
这个类的内部结构比较简答,看过1.7版本及以前版本的HashMap的同学应该比较熟悉,就是一个简易的HashMap结构,唯一不同的是这个Entry是继承了WeakReference弱引用,这个在第三部分会详细说。
这个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();
}
首先是获取当前的线程,然后通过getMap方法来获取一个ThreadLocalMap的对象,这个就是上面我们说的ThreadLocal内置的一个Map的结构。
然后如果这个Map对象不为空,则找到里面的Entry,然后获取其中的value返回结果。
这里面有一个在面试中会比较经常问答到点,就是ThreadLocal获取本地变量的时候,Key是什么值?通过上面的代码我们看到,是用的this引用,这个this就是这个ThreadLocal对象的引用。所以这里要特别注意,在获取ThreadLocal的Entry的时候,key是ThreadLocal的引用。
private Entry getEntry(ThreadLocal<?> key)
接下来我们看一下getMap方法里面是怎么获取到的ThreadLocalMap对象的:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
很简单,直接通过当前线程,返回threadLocals成员变量,那么有的同学就好奇了?这个在ThreadLocal中定义的内部类,怎么又成为Thread里面的一个成员变量了呢?
带着你的好奇,我们再来看一下get方法,上面我们是假设通过当前线程能够获取到ThreadLocalMap对象,那么如果获取不到呢?
就会执行``` setInitialValue ``` 方法,那么我们就看看这个方法里面做了什么:
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
首先通过initialValue方法获取初始值,然后获取当前线程,那么还是之前的流程,只不过在else分支中,这里如果获取不到ThreadLocalMap对象,那么就会调用createMap方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
看到这里就很清晰了吧,这里创建了一个ThreadLocalMap对象,然后赋值当先线程的threadLocals成员变量。
到这里基本上ThreadLocal的核心设计逻辑基本就清楚了,所以下面我用自己比较通俗易懂的话概括总结一下:
ThreadLocal在自己的内部类ThreadLocalMap中保存线程的本地变量,然后ThreadLocalMap对象又是以成员变量的角色存在与Thread里面,是不是觉得很绕,那为什么不直接把ThreadLocalMap对象直接定义到Thread里面呢?其实是这样,ThreadLocalMap存在Thread里面,可以方便的与线程关联起来,否则就需要在ThreadLocal中去维护所有线程的ThreadLocalMap对象,这样增加了查找和使用的开销。放到Thread里面在使用的时候则非常的方便。
还有一个问题有的面试官也会问到,就是为什么要设计ThreadLocalMap,直接定义一个成员变量来存储线程本地变量值不就好了吗?我想上面我已经说清楚了,ThreadLocalMap是与线程绑定的,是Thread类的成员变量,可以这么理解,一个ThreadLocal对应一个Thread,但是一个Thread里面可以存在多个ThreadLocal啊,只是在第一次调用的时候会初始化ThreadLocalMap对象,后面就会通过ThreadLocal的引用,设置不同的成员变量,所以会设计ThreadLocalMap,然后通过Entry的方式来管理。
三、内存泄漏问题
很多人因为听到内存泄漏而变得害怕,有些场景其实比较适合使用ThreadLocal,但是因为搞不懂为什么会出现内存泄漏,进而放弃使用ThreadLocal,其实我认为大可不必,下面我们一起来看看在什么情况下会产生内存泄漏?产生内存泄漏的原因又是什么?这样下次就不会因为害怕内存泄漏而不使用ThreadLocal了。
首先解释一下上一小节中我们提到的,ThreadLocalMap里面的Entry数据结构是继承了WearReference弱引用,这里我们先简单回顾一些Java中的四种引用类型:
- 强引用(Strong Reference):这个比较好理解,就是我们通过new,或者是反射创建的对象,只要我们不手动释放,就一直不会被垃圾回收调,这种对象就是强引用,大家都比较熟悉。
- 软引用(Soft Reference) :软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
- 弱引用(Weak Reference):弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。
- 虚引用(Phantom Reference):虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收,在 JDK1.2 之后,用 PhantomReference 类来表示,通过查看这个类的源码,发现它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。
这里为了便于理解,我手动画了个图,下面的内容我们结合图来进行说:

具体产生泄漏的原因我已经在图片中写了一下。下面再详细的说明一下:
我们说ThreadLocalMap中的key是一个弱引用对象,那么这个引用对象被GC回收掉,导致存在于Entry中的value无法被回收,所以出现了内存泄漏。这个问题其实ThreadLocal已经做了兜底的处理来帮助我们避免这样的问题,就是在我们执行get,set,remove 的方法的时候,都会检查一遍当前的Entry , 如果存在key为null的Entry会自动帮我们清理掉,这样就能够大概率的避免内存泄漏的出现。但是如果在线程中我们一直不去调用对应的方法,那内存泄漏就会一直存在,所以说最好的习惯就是在我们不需要使用ThreadLocal的时候,显示的调用remove方法。
一般的线程我们使用完成之后,随着线程生命周期的终结对应的ThreadLocal也就被回收了,所以不太容易出现内存方面的问题,但是如果我们使用的是线程池呢?对于corePollSize的线程是会被重复利用的,这个时候如果我们不显示的调用一下remove方法,带来的就不仅仅是内存的问题,而是更加严重的业务逻辑问题了,而且还特别的不好重现,增加了很大的排查难度。
所以如果在线程池中我们使用到了ThreadLocal,一定要重写一下afterExecute方法,在其中释放掉ThreadLocal信息,保证程序不会出现不可预知的错误。

浙公网安备 33010602011771号