前言
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
方法清除数据。