ThreadLocal<T>原理解析
一、基本使用及含义
1.ThreadLocal<T>,直译过来叫线程本地变量,线程隔离。
文档注释:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
大概意思是ThreadLocal这个类的变量可以通过get和set方法读写一个线程自己独立的变量副本;
ThreadLocal实例通常是类的私有静态属性,将状态与之对应的线程相关联。
换句话来说就是,一般我们使用ThreadLocal时,将其定义为private static,目的是把一些状态的值和当前的线程进行隔离保存,每个线程只能读写自己的状态内容。
2.简单使用
比如,web应用中统计每个请求的耗时,可以通过aop+ThreadLocal来实现(计算请求的消耗时间只是为了简单使用ThreadLocal,实际开发中不会这样做)。
@Aspect public class RequestMonitor { /* 计算Controller方法执行的总时间:Aop中计算,不用在Controller的每个方法中 消耗时间 = after()中的结束时间 - before()中的开始时间 因为是多线程并发访问,所以不能直接定义成员变量 */ //线程隔离,用于记录每个请求初始内存指标 - private static ThreadLocal<Long> startData = new ThreadLocal<>(); //切入点 所有的Controller中的所有方法 @Pointcut("execution(* xxx..controller.*Controller.*(..))") public void handle(){ } @Before("handle()") public void before(){ //记录当前线程初始内存分配 单位:字节 Long startTime = System.currentTimeMillis(); startData.set(startTime); } @After("handle()") public void after(){ //从ThreadLocal拿到当前线程的初始数据 Long startTime = startData.get(); Long endTime = System.currentTimeMillis(); Long cost = endTime - startTime; //把得到的数据做其他的处理,一般是MQ或者Redis,尽量不影响当前请求的性能 } }
每个请求(线程)get到的数据是自己set的数据!
3.应用场景
(1)Spring的声明式事务,获取数据库连接时,一个事务里(同一个线程),第一次从数据库连接池中获取,后面的都从ThreadLocal中获取,这样就能保证是同一个Connection;
(2)多线程读写共享变量时用于线程隔离操作;
(3)Session管理等。
二、源码解析
1.ThreadLocalMap
ThreadLocal的静态内部类,作为Thread类的成员变量,由Thread负责管理其生命周期。

ThreadLocalMap内部维护了一个Entry数组(可以resize进行扩容,初始容量为16,容量达到2/3时就开始扩容),类似于HashMap,也是一个key-value数组,以ThreadLocal作为key,需要保存的值作为value。至于上图红框中ThreadLocal为什么由一个弱引用持有,后面会讲。
Thread管理ThreadLocalMap如下:
(1)Thread类声明ThreadLocalMap成员变量:

(2)Thread.init()中初始化:

(3)ThreadLocal中,创建线程t的ThreadLocalMap:

(4)当线程结束或者因异常打断而退出时,会自动调用exit()方法,里面会将ThreadLocalMap置为null,生命周期也就结束:

综上,当一个线程创建时或ThreadLocal第一次set时,会初始化ThreadLocalMap,并有线程自己管理其生命周期。
当一个线程set的时候,如果ThreadLocalMap还没有初始化,则ThreadLocal会为这个线程初始化一个ThreadLocalMap并将引用赋值到Thread.threadLocals,在exit()中回收,也就是线程退出的时候ThreadLocalMap就被回收。
2.ThreadLocal
set(T value):

首先获取当前调用set()的线程,然后获取这个线程的ThreadLocalMap(t.threadLocals);如果map为null,则创建一个ThreadLocalMap并将引用赋值给当前线程的threadLocals变量;如果不为null,则调用ThreadLocalMap.set(ThreadLocal<?> key,Object value)
private void set(ThreadLocal<?> key, Object value) { //获取当前ThreadLocalMap的Entry数组 Entry[] tab = table; //数组的长度 int len = tab.length; //计算key的下标位置,hashcode和长度位运算,固定 int i = key.threadLocalHashCode & (len-1); //tab[i] != null,表示当前线程已经使用这个ThreadLocal变量已经调用过set for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //获取下标i位置的ThreadLocal对象并比较 ThreadLocal<?> k = e.get(); //如果相等,直接覆盖之前的值(同一个线程多次调用set()时只保留最后一个) if (k == key) { e.value = value; return; } //如果k==null,大概表示ThreadLocal变量已经被回收,但程序中没有调用remove(),导致value的值一直占据着内存(内存泄漏) if (k == null) { replaceStaleEntry(key, value, i); return; } } //如果tab[i] == null,表示Entry数组中还没有存放当前ThreadLocal变量的值,那么就会创建一个Entry对象 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
大概流程是:先判断在Entry数组指定下标位置(数组长度不变的情况下,同一个ThreadLocal对象set时,会把值存入到Entry数组固定下标的位置)是否为null,如果不为null,则直接在此下标处新建一个Entry(当前ThreadLocal变量,值);否则,比较传入的ThreadLocal和这个位置上的ThreadLocal是不是同一个对象,如果是则直接替换value,如果可用为null,则将传入的ThreadLocal变量和value直接替换。
总之,ThreadLocal.set()是把当前这个ThreadLocal变量和value封装成一个Entry,存放到线程的ThreadLocalMap变量中,这样就做到了线程隔离。
注:不同的ThreadLocal对象操作同一个线程的ThreadLocalMap,get/set操作会把ThreadLocal变量自身作为key传入,放在Entry数组不同下标位置,这样就能区分不同的ThreadLocal对象放在同一个线程中的值。
get():

直接从当前线程的ThreadLocalMap中,Entry数组固定的下标位置处获取值;
如果在数组该位置处的key和传入的key指向同一个ThreadLocal(同一个引用),则直接返回Entry;否则,需要循环在数组的其他位置查找,如果找不到则返回null。

三、ThreadLocal中的两种内存泄漏问题

1.在类中声明的变量:ThreadLocal<T> tl = new ThreadLocal<>();此时tl是一个强引用指向堆中的ThreadLocal对象,同时ThreadLocalMap中的Entry对象是以同一个ThreadLocal作为key,
有些时候,当线程中的tl变量可能已经没有指向堆中的ThreadLocal对象,但Entry中的key还指向这个ThreadLocal对象,如果Entry中的key也是一个强引用指向就导致堆中的ThreadLocal对象永远都不能被垃圾回收,最终内存泄露。
所以JDK定义Entry的时候继承自WeakReference<ThreadLocal<?>>,key持有的弱引用指向堆中的ThreadLocal,这样当线程中的tl变量强引用不再指向堆中的ThreadLocal时,由于弱引用的特性Entry中的key可以在下一次GC时被回收调,也就不会存在ThreadLocalMap中的key引起的内存泄露问题。

2.当ThreadLocal对象被回收了之后,由于某些线程运行的时间很长甚至一直在后台运行,就不会执行exit()方法,ThreadLocalMap就不会被回收,由于key指向堆中的ThreadLocal对象已经变成了null,那么value也就不能通过key被访问,但实际上value对象还一直在堆中存在占据着内存,所以还是会存在内存泄漏的问题。
这种内存泄漏的问题解决需要在程序中显示地调用ThreadLocal.remove()方法,所以在当前线程不再需要指定的ThreadLocal保存的数据时,就应该调用remove()。


e.clear():Reference中定义的,直接将当前的Entry引用置为null,GC可以回收。
expungeStaleEntity():清除掉无用的Entity,将key指向的value设置为null。

浙公网安备 33010602011771号