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。

posted @ 2020-09-03 17:41  沐先生forever  阅读(397)  评论(0)    收藏  举报