ThreadLocal存在不存在内存泄漏,趁此机会和大家聊聊ThreadLocal到底存在不存在内存泄漏以及怎么避免。
Thread中的threadLocals属性
一切都要从 Thread
的一个属性 threadLocals
说起,让我们看下这个属性的介绍:
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
这个 threadLocals
属性是一个 ThreadLocal
里的静态类ThreadLocalMap
,它是一个 map
,并且是由 ThreadLocal
进行维护管理的。
那么这个 threadLocals
,也就是这个 map
里,存的是什么呢?
我们来看 ThreadLocalMap
:
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } }
介绍里说,ThreadLocalMap
是一个定制化的 hash map
,在 Entry
里,以键值对的形式存储着 ThreadLocal
对象和 value
。
于是 Thread
中 threadLocals
属性和 ThreadLocal
的关系简图就如下所示:
threadLocals.png
注意,这里 Entry
里的 key
,即 ThreadLocal
对象是以弱引用的形式存在的,这将是本文内存泄露分析的重点之一,但这里先不谈,再继续讲讲 Thread
和 ThreadLocal
的关系。
Thread和ThreadLocal的关系
先上一段示例代码:
public class ThreadLocalDemo { private static ThreadLocal<Weapon> weaponThreadLocal = new ThreadLocal<Weapon>() { @Override protected Weapon initialValue() { return new Weapon(); } }; private static class Player extends Thread { @Override public void run() { weaponThreadLocal.get().level += ThreadLocalRandom.current().nextInt(5); System.out.println(getName() + " level: " + weaponThreadLocal.get().level); weaponThreadLocal.get().combatEff = weaponThreadLocal.get().level * 10; System.out.println(getName() + " combatEff: " + weaponThreadLocal.get().combatEff); } } public static void main(String[] args) { Player player1 = new Player(); Player player2 = new Player(); player1.start(); player2.start(); } private static class Weapon { int level; int combatEff; public Weapon() { level = 1; combatEff = 10; } } }
在上述代码中,有两个 Player
,他们进行一场游戏,每个人在游戏开始时都会有一把武器 Weapon
。这把武器在游戏开始时对每个人来说是公平的,它的等级(level
)和战斗力 (Combat Effectiveness
)都是一个固定值(由 ThreadLocal
初始化)。随着游戏的进行,他们的武器等级会升级,战斗力会变强,但升多少级、变强多少就看造化了(由 ThreadLocalRandom
产生随机数)
看看运行之后的结果吧:
Thread-1 level: 3 Thread-0 level: 5 Thread-1 combatEff: 30 Thread-0 combatEff: 50
看来线程0运气更好一点。
好了,上述的例子只是为了接下来的说明做一个铺垫,下面就从上述例子开始谈谈 Thread
和 ThreadLocal
的关系。
一般来说,可以认为 ThreadLocal
解决了线程间共享变量的问题,即 ThreadLocal
为每个线程维护了一个共享变量的副本,多个线程在修改这个变量时(其实是修改自己的变量副本),不存在线程安全问题,效率也很高。所以,在上述例子中,对于一个共享变量,ThreadLocal
提供了两个功能:
- 统一设置初始值
- 每个线程对该值的修改互不影响,做到变量隔离
那么乍一看,Thread
和 ThreadLocal
的关系好像是这样:
threadLocal-key-value.png
可是这样是不对的,如果理解成这样,我上面 Thread中的threadLocals属性
那一大段就白说了。
再把那段的内容概括下,Thread
里有 ThreadLocalMap
,而ThreadLocal
和 value
以键值对的形式存储在 ThreadLocalMap
中,所以 Thread
和 ThreadLocal
的关系应该是这样:
thread-threadLocal.png
当我们调用 ThreadLocal
的 get()
、set()
和 remove()
操作 Thread
对应 的 value
时,实际上是由 Thread
的 ThreadLocalMap
在操作 ThreadLocal
对应的 value
。
对应上述的代码示例,如果我们再给每个 Player
新增一个 Life
的共享变量,又多出一个管理 Life
变量的 ThreadLocal
,那么它们的示意图就该是这样的:
add-life.png
至此,Thread
和 ThreadLocal
的关系应该说明清楚了,下面就开始分析 ThreadLocal
中存在的内存泄露问题。
ThreadLocal中内存泄露问题分析
要分析 ThreadLocal
中的内存泄露问题,得看一张 Thread
和 ThreadLocal
从内存角度分析的关系图:
memory.png
从上图进行后续分析。
分析一
从上图可知,Thread
对象里的 threadLocals
持有 ThreadLocalMap
对象,Entry
对象。那么当线程执行完毕,线程对象被回收,ThreadLocalMap
也会被回收。由于 Entry
持有 Weapon
对象,即 value
对象的引用,value
对象也会被回收。除了 ThreadLocal
对象,随着线程执行完毕,所有对象都会被回收,皆大欢喜,没有内存泄露。
分析二
若线程还在执行中,而 ThreadLocal
对象引用被置为 null
,即现在不需要 ThreadLocal
了,那么其实 Weapon
也失去了意义,照理说是该把 Weapon
对象回收的,那么怎么回收呢?
一旦 ThreadLocal
对象引用被置为 null
,那么由于 Entry
对象持有的是 ThreadLocal
对象的弱引用,那么 ThreadLocal
对象就会在下一次 YGC
时被回收。此时,Entry
对象的 key
为空了,value
无法访问到了,怎么回收呢?原来对此情况早有设计,当每次在 get()
、set()
、remove()
ThreadLocalMap
中的值的时候,都会自动将 key
为空的 value
置为空,那么 value
对象也能够被回收了,不存在内存泄露了。
那么内存泄露到底存在于哪里呢?
分析三
在我们使用 ThreadLocal
时,通常是将它作为私有静态变量使用的。如果把 ThreadLocal
作为成员对象使用,那么每个使用的 ThreadLocal
的类都可能创建一个 ThreadLocal
对象,而 ThreadLocal
其实是使用 ThreadLocalMap
对线程和 value
进行管理的,多个 ThreadLocal
对象没有意义,会造成内存浪费。
但另一方面,把 ThreadLocal
作为静态变量使用的话,它就无法被置空了。ThreadLocal
无法被置空,就无法通过触发弱引用机制来回收 ThreadLocal
对象,Entry
里的 key
就不会为空,就无法通过分析二的方法回收 value
对象。
这就是内存泄露的由来了。
总结一下内存泄露的两个条件:
ThreadLocal
作为静态变量使用- 线程未执行完毕
在此情况下,线程中的 ThreadLocalMap
中的键值对会越堆越多,可能产生内存溢出问题。
解决办法
如果线程还在执行,那么在 ThreadLocal
的使命完成后,调用它的 remove()
方法,该方法会把 Entry
里的 key
置空,就可以回收 value
对象了。(这里 remove()
方法还有待研究),但用就对了。
线程池脏数据分析
再分析一下 ThreadLocal
和线程池一起使用时的脏数据问题。(其实 ThreadLocal
的内存泄露也多数出现在和线程池一起使用的情况)
由以上分析可知,线程执行完毕,线程对象被回收,一切问题都不会存在。
若线程在线程池中复用,且不调用 remove
方法,那么线程在执行完毕一次任务并复用时,从 ThreadLocalMap
中取出来的 value
就是上一次执行任务完毕后的值。这时候,倘若我们的线程在执行每次任务时,没有调用 set()
方法对 value
重新赋值,那么业务逻辑肯定就错了。
解决办法
- 线程池复用时,在线程的
run()
方法中要调用ThreadLocal
的set()
方法对value
重新赋值 - 在线程的
run()
方法最后调用ThreadLocal
的remove()
方法