在多线程环境下,比如在spring容器类中定义的变量,如何防止自己的变量被其它线程篡改。想要解决这个问题,不得不提到一个类:ThreadLocal
ThreadLocal的业务场景是什么?我们怎么用好这个类?你知道ThreadLocal的内存泄漏吗?当我们知道ThreadLocal的原理之后,这个问题也会迎刃而解。
我们首先看一下ThreadLocal的常用Api实现
set过程
1 | public void set(T value) { |
步骤讲解
- 首先获取当前线程
- getMap是指从当前线程中获取到线程的threadLocals,threadLocals是线程中的一个静态内部类ThreadLocalMap
变量为threadLocals1
2
3ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
1 | ThreadLocal.ThreadLocalMap threadLocals = null; |
- 判断获取到的threadLocals是否为空,不为空插入数据,this为当前Threadlocal对象,value为设置的值,set过程过还伴随着清除的过程,若空间不够,还会对全部的entry进行扫描清除,进行resize
- 否则创建threadLocals并设置值,
创建ThreadLocalMap过程ThreadLocalMap内部又是一个继承了弱引用的ThreadLocal的entry数组1
2
3void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
new ThreadLocalMap(this, firstValue) 的过程
1 | ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { |
首先初识化一个Entry数组,entry的key持有ThreadLocal的弱引用,简单介绍一下,弱引用会在jvm发生gc时没有其他强引用关联时而断开弱引用链进行垃圾回收。稍后我会对这个情况进行分析。其次通过threadLocalHashCode找到在entry中的位置,放入到entry中,最后初始化threshold,超过threshold即2/3自动扩容
1 | setThreshold(INITIAL_CAPACITY); |
归纳一下上面的流程,简要的来说就是ThreadLocal只是一个符号,并不存储任何值,通过当前线程中的ThreadLocalMap存储值,ThreadLocalMap中有一个entry数组,entry数组继承弱引用,在entry放入key,value时,entry会调用父类的构造方法,意思就是entry的key只持有ThreadLocal的弱引用。
1 | Entry(ThreadLocal<?> k, Object v) { |
找到一张著名的网图说明一下这个关系链
get过程
在我们熟悉set过程之后,get过程更好理解了
1 | public T get() { |
- 首先获取当前线程
- 获取到当前线程的ThreadLocalMap
- 判断ThreadLocalMap是否为null,为null则跳到第6步初始化
- 获取当前threadLocalHashCode对应的entry值
- 返回entry的value值,就是在业务中往ThreadLocal设置的那个值
- 若为null,初始化值
看一下初始化值的操作上面段代码简要分析就是初始化值,initialValue返回固定返回一个null,然后下面几段代码在get方法中已经分析了,这里就不在分析。总之就是把null放到entry的value位置,返回null。1
2
3
4
5
6
7
8
9
10private 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;
}
需要注意的是在map.getEntry(this);这里的时候,还额外做了一个操作,就是判断获取到的entry或者entry的key是否为null,因为ThreadLocalMap使用线性地址法存储值,所以还需要依次找下去,直到entry为空为止,并清除掉key为空的entry
getEntryAfterMiss(key, i, e),核心的清除算法见文底,也可以看尾部其他作者讲解threadLocal的详细源码过程。
1 | private Entry getEntry(ThreadLocal<?> key) { |
remove过程
remove相对于其他两个方法比较简单,核心方法如下所示
1 | private void remove(ThreadLocal<?> key) { |
- 找到threadLocalHashCode所在的位置,也就是threadLocal在entry中的位置
- 因为Entry是以线地址法解决冲突而放进去的值,所以需要沿着线遍历找到正确的值
- 清除threadlocal的引用
- 对后续的值进行段式清除
其中主要的是expungeStaleEntry这个方法,这也是get、set方式清除无效引用的核心方法,以下解析截取自https://www.cnblogs.com/micrari/p/6790229.html
1 | /** |
执行原理如下图所示

ThreadLocal的常见问题
- gc后key一定会为null吗,get为什么还可以获取到值
在我们使用ThreadLocal的时候,常使用全局的final static来修饰ThreadLocal变量,这种变量的生命周期和jvm一样长,存放在jvm的方法区内,会一直保持有保持对ThreadLocal的强引用,所有不管jvm何时gc,只要我们持有这种引用关系,ThreadLocal就不会被回收,ThreadLocal不被回收,弱引用关系自然也存在,我们也就可以获取到他的值。举个String类型的弱引用看一下就可以很清楚的明白这个关系,ThreadLocal道理也是如此
1 | WeakReference<String> staticString=new WeakReference<>(test); |
2. 为什么ThreadLocalMap内部是entry数组,而不是单个entry
因为在一个线程中,不止我们使用的ThreadLocal,还存在其他地方设置的ThreadLocal,比如Tomcat内,在idea环境中看一下实际情况
在我们还没有设置ThreadLocal自己的值的时候,在线程内已经存在了三个ThreadLocal设置的值,所有说需要我们用一个Entry数组来接收不同的ThreadLocal对象设置的值
3. 为什么ThreadLocalMap中的Entry不用强引用,而继承WeakReference
因为在我们开发过程中,经常使用线程池进行线程的链接复用,如果用强引用的话,会造成我们已经离开的ThreadLocaL变量的作用域,而ThreadLoca却迟迟得不到回收,下面我通过画一张图来说明这个情况
若使用强引用方式,在ThreadLocal ref断开之后,但是entry中的key和ThreadLocal对象的引用却没有断开,在之后也没有对ThreadLocal进行remove,在后续线程不进行使用或者不正确的使用下,则会发生内存泄漏或者数据混乱
- 真的会内存泄漏吗
在正常情况下,我们对线程池进行使用,在对在ThreadLocal进行set、get时ThreadLocalMap内部都会对entry数组进行expungeStaleEntry操作,在扩容时还会进行全量清除,在网上的篇子中常说会持有 当前线程->当前线程的threadLocals(ThreadLocal.ThreadLocalMap对象)->Entry数组->某个entry.value】,并且key为null的情况,但是在同一个线程对同一个ThreadLocal进行expungeStaleEntry会自我清理,在set的时候又会重新设置进这个位置,key为null的情况又被还原了,因为threadLocalHashCode在同一个ThreadLocal对象下值不会变。除非线程池线程不在使用,或者ThreadLocalMap无法扩容或者无法遍历到entry中这个位置,这种情况下才会发生内存泄漏,但是为了安全,请保持好良好的编码习惯,及时remove
5. 为什么会数据混乱
在我们对ThreadLocal进行使用的时候,没有良好的编码习惯,对ThreadLocal没有进行remove,并且在进行逻辑判断的时候还意外的进入了get方法内,没有进行set,这个时候就会把上次设置进的值重新取出来,造成数据的紊乱。举个列子:
1 | { |
只取一个线程,代表复用这个线程,不断的执行业务代码,当我们第一次进入到(int)(Math.random()*1000000)的时候,设置进了值,但是我们没有remove,在下次判断的时候又没有进入set里面,导致还是取用了上次的值,导致数据混乱,所以说还是要养成良好的编码习惯,及时remove
以下是我看过的最精彩的一篇ThreadLocal源码文章,详细讲解了阶段清除和全量清除的步骤: