前言
ThreadLocal是一个本地线程副本变量存储工具类。主要用于将一个线程和该线程存放的副本对象做一个映射(Map),各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不同的变量值完成操作的场景。
正文
我们根据代码了解下ThreadLocal。
在了解ThreadLocal之前,我们先带着几个问题:
ThreadLocal的每个线程的私有变量保存在哪里?- 大家说的关于
ThreadLocal使用不当会发生内存泄露又是怎么回事? ThreadLocal弱引用导致内存泄露又是怎么回事?为什么要使用弱引用?- 对于可能出现的内存泄露,
ThreadLocal本身有哪些优化?我们编码时应该如何避免内存泄露? ThreadLocal的应用场景?
首先我们先来看下ThreadLocal的几个重要方法。
ThreadLocal的主要方法
ThreadLocal有三个重要方法,如下:
| 方法 | 说明 |
|---|---|
| public T get() | 该方法用于获取线程本地变量副本 |
| public void set(T value) | 该方法用于设置线程本地变量副本 |
| public void remove() | 该方法用于移除线程本地变量副本 |
三个方法的相关源码:
1 | public T get() { |
由上面get()方法的源码可以看到,本地变量副本是由一个叫ThreadLocalMap的对象维护的,我们看一下getMap(t)方法。
1 | ThreadLocalMap getMap(Thread t) { |
1 | public class Thread implements Runnable { |
可以看到在Thread类里维护着一个ThreadLocalMap,该线程的本地变量副本就会存到这儿。
再来看下这个变量赋予初始值的过程。
1 | private T setInitialValue() { |
当我们通过set方法设置本地变量副本时,如果ThreadLocalMap为null,就会调用createMap将初始值放入。
而对于get方法,如果ThreadLocalMap为null,就会调用setInitialValue方法,最终调用createMap方法,此时初始值为null。
我们继续看下ThreadLocalMap的相关源码。
ThreadLocalMap
ThreadLocalMap的主要代码如下:
1 | static class ThreadLocalMap { |
可以看到ThreadLocalMap内部是通过Entry的value来维护变量副本的,其key为ThreadLocal本身。
而且Entry的key为弱引用(WeakReference)。
关于Java引用
Java中的引用按照引用强度不同分为四种,从强到弱依次为:强引用、软引用、弱引用和虚引用。
引用的强度,代表了对内存占用的能力大小,具体体现在GC的时候,会不会被回收,什么时候被回收。
强引用
我们一般很少提及它,但它无处不在。其实我们创建一个对象便是强引用,如StringBuffer buffer = new StringBuffer();。
HotSpot JVM目前的垃圾回收算法一般默认是可达性算法,即在每一轮GC的时候,选定一些对象作为GC ROOT,然后以它们为根发散遍历,遍历完成之后,如果一个对象不被任何GC ROOT引用,那么它就是不可达对象,则在接下来的GC过程中很可能会被回收。
如果我们在垃圾回收时还有对buffer的引用,那么它便不会被垃圾回收器回收。
软引用
软引用是用来描述一些还有用但是并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收返回之后进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。JDK1.2之后提供了SoftReference来实现软引用。
相对于强引用,软引用在内存充足时可能不会被回收,在内存不够时会被回收。
弱引用
弱引用也是用来描述非必须的对象的,但它的强度更弱,被弱引用关联的对象只能生存到下一次GC发生之前,也就是说下一次GC就会被回收。JDK1.2之后,提供了WeakReference来实现弱引用。
虚引用
虚引用也成为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间造成影响,也无法通过虚引用来取得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是在这个对象被GC时收到一个系统通知。JDK1.2之后提供了PhantomReference来实现虚引用。
ThreadLocal内存模型
由上面内容,下面ThreadLocal的内存模型也是比较好理解的。

线程运行时,我们定义的
ThreadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef。当
ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的ThreadLocalMap实例是否被创建,如果没有,则创建并初始化。Map实例化之后,也就拿到了该ThreadLocalMap的句柄,然后如果将当前ThreadLocal对象作为key,进行存取操作。图中的虚线,表示
key对ThreadLocal实例的引用是个弱引用。
内存泄露分析
根据上面内容,我们可以知道 ThreadLocal是被ThreadLocalMap以弱引用的方式关联着,因此如果ThreadLocal没有被ThreadLocalMap以外的对象引用,则在下一次GC的时候,ThreadLocal实例就会被回收,那么此时ThreadLocalMap里的一组Entry的K就是null了,因此在没有额外操作的情况下,此处的V便不会被外部访问到,而且只要Thread实例一直存在,Thread实例就强引用着ThreadLocalMap,因此ThreadLocalMap就不会被回收,那么这里K为null的V就一直占用着内存。
因此发生内存泄露的条件是:
ThreadLocal没有被外部强引用;ThreadLocal实例被回收;- 但是
Thread实例一直存活,一直强引用着ThreadLocalMap,也就是说ThreadLocalMap也不会被GC。
也就是说,如果ThreadLocal使用不当,是有可能发生内存泄露的。
我们这里说的内存泄露,指的是开发者使用不当造成的,而非
ThreadLocal本身的问题。
一个典型的例子就是线程池,如果我们在线程池的task里实例化了ThreadLocal对象,线程使用完后,回归线程池,但是本身并不会结束,但是task任务结束了,对ThreadLocal的强引用结束了,这时候在ThreadLocalMap中的value没有被任何清理机制有效清理。
我们可以模拟这种内存泄露情况,代码如下:
1 | public class ThreadLocalTest { |
如上,我们模拟了一个长度为1的定长线程池(为了简化),这个线程池只有一个线程,我们在task里创建了ThreadLocal对象,当task结束后,实际Thread是还存活的。
我们通过debug模式,执行若干次,可以看到ThreadLocalMap里那些无用的value,如下图:

这实际上就发生了内存泄露问题。
其实,我们调用ThreadLocal里提供的remove方法,变会完全解决这个问题。

如上图,我们如果使用完后添加remove方法删除变量副本,可以看到无论运行多少次,也不会出现内存泄露问题。
不要觉得这个内存泄露条件自己不会碰到,实际上无论Http,数据库连接等都有线程池的概念,我们每一段代码如果使用
ThreadLocal都可能成为task那段的一部分,使用不好就可能出现内存泄露问题。因此在日常编码中一定要养成良好的编码习惯。
ThreadLocal的优化
如果上面那个内存泄露的例子我们多运行一段时间,跟着debug,会发现ThreadLocalMap并不会一直增长的。
如下图,可以看到一些无用数据会自动消失。

这是因为ThreadLocal本身的优化,在ThreadLocalMap的getEntry方法里,我们可以看到如下方法getEntryAfterMiss:
1 | private Entry getEntry(ThreadLocal<?> key) { |
是的,这个方法就是找不到Entry的处理方法,该方法代码如下:
1 | private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { |
这个方法一个关键部分就是k == null时调用expungeStaleEntry方法,用来删除旧的Entry,代码如下:
1 | private int expungeStaleEntry(int staleSlot) { |
主要逻辑如下:
清理当前脏
entry,即将其value引用置为null,并且将table[staleSlot]也置为null。value置为null后该value域变为不可达,在下一次gc的时候就会被回收掉,同时table[staleSlot]为null后以便于存放新的entry;从当前
staleSlot位置向后环形(nextIndex)继续搜索,直到遇到哈希桶(tab[i])为null的时候退出;若在搜索过程再次遇到脏
entry,继续将其清除。
除了该方法外,我们在set方法里可以看到对脏entry的处理,如下:
1 | private void set(ThreadLocal<?> key, Object value) { |
在该方法中针对脏entry做了这样的处理:
如果当前
table[i]!=null的话说明hash冲突就需要向后环形查找,若在查找过程中遇到脏entry就通过replaceStaleEntry进行处理;如果当前
table[i]==null的话说明新的entry可以直接插入,但是插入后会调用cleanSomeSlots方法检测并清除脏entry
我们先来看下replaceStaleEntry方法。
1 | private void replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot) { |
这部分代码通过PreIndex方法实现往前环形搜索脏entry的功能,初始时slotToExpunge和staleSlot相同,若在搜索过程中发现了脏entry,则更新slotToExpunge为当前索引i。另外,说明replaceStaleEntry并不仅仅局限于处理当前已知的脏entry,它认为在出现脏entry的相邻位置也有很大概率出现脏entry,所以为了一次处理到位,就需要向前环形搜索,找到前面的脏entry。
我们再来看下cleanSomeSlots方法的调用。
1 | private boolean cleanSomeSlots(int i, int n) { |
该方法用来清除一些脏entry,其扫描次数通过n来控制,可以看到n >>>= 1表示每次n除以2进行减小范围搜索,当遇到脏entry时,n = len,就会扩大搜索范围。如果在整个搜索过程没遇到脏entry的话,搜索结束,采用这种方式的主要是用于时间效率上的平衡。
为什么使用弱引用?
根据上面的内容,我们来分析下为什么ThreadLocal要使用弱引用。
假设ThreadLocal使用的是强引用,在业务代码中执行threadLocalInstance==null操作,以清理掉ThreadLocal实例的目的,但是因为ThreadLocalMap的Entry强引用ThreadLocal,因此在gc的时候进行可达性分析,ThreadLocal依然可达,对ThreadLocal并不会进行垃圾回收,这样就无法真正达到业务逻辑的目的,出现逻辑错误。
假设Entry弱引用ThreadLocal,尽管会出现内存泄漏的问题,但是在ThreadLocal的生命周期里(set,get,remove)里,都会针对key为null的脏Entry进行处理。
从以上的分析可以看出,使用弱引用的话在ThreadLocal生命周期里会尽可能的保证不出现内存泄漏的问题,达到安全的状态。而且只要我们规范代码,就可以避免内存泄露问题。
线程退出
在Thread源码里,我们可以看到exit方法。
1 | private void exit() { |
可以看到线程退出后,threadLocals变为null,也就意味着GC可以将ThreadLocalMap进行垃圾回收。
ThreadLocal应用场景
ThreadLocal在一些开源框架下有着广泛应用。
Spring的事务管理
在Spring事务管理相关类
TransactionAspectSupport代码中,我们可以找到这段代码.1
2
3
4//...部分代码略
private static final ThreadLocal<TransactionInfo> transactionInfoHolder =
new NamedThreadLocal<TransactionInfo>("Current aspect-driven transaction");
//...部分代码略其目的就是用来存储当前事务相关信息。
Logback中的使用
在Logback的LogbackMDCAdapter相关代码中,也有ThreadLocal的使用。
1 | //...部分代码略 |
- 在
tomcat相关代码中,org.apache.catalina.core.ApplicationContext。
1 | //...部分代码略 |
如果要配置多数据源,我们可以使用
ThreadLocal来进行数据源key的切换管理。可以看下这篇文章SpringBoot多数据源配置
结语
我们对Threadlocal进行了详细介绍,除了了解它的主要原理,解决项目中遇到的一些问题外,更要使用好它,每次使用完Threadlocal,应调用remove方法清除数据。

