ThreadLocal 源码解析及常用业务场景

616 阅读5分钟

ThreadLocal简介

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。 ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景

总而言之,什么是TreadLocal? 答:给独立的线程提供局部变量的缓存的工具.

模型如下图:

image.png

ThreadLocal 的核心API就两个 GET , SETremove,看下源码

public void set(T value) {
       // 值存在哪里? 是当前的线程
    Thread t = Thread.currentThread();
    // 线程持有的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //不为空  this 就是当前的TreadLocal,value 就是我们要设置的值
    if (map != null)
        map.set(this, value);
    else
    //为空 给当前线程t新创建一个ThreadLocalMap 并赋值
        createMap(t, value);
}
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

根据SET方法源码可以看出,ThreadLocal 是具有线程隔离性的(线程安全),对值进行的SET操作是针对当前线程的,GET 和 REMOVE方法也一样针对的都是当前线程,通过以下代码可以简单明了的看一下:

public class ThreadLocalTest extends BaseTest{

    private static ThreadLocal<String> threadLocal =new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {

        Thread thread =new Thread(()->{
           threadLocal.set("Leslie");
            System.out.println("子线成赋值成功");
            System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
        });
        thread.start();
        //保证子线成赋值成功
        thread.join();

        System.out.println(Thread.currentThread().getName()+":"+threadLocal.get());
    }
}

打印结果:

子线成赋值成功
Thread-0:Leslie
main:null

可以看出 ,因为是在子线程中执行的SET,所以ThreadLocal 缓存的值也是针对子线程的,所以子线程可以通过GET拿到值,但是主线程不行.

ThreadLocal 的应用场景有哪些?

1.其实最常见的就是 ServletRequestAttributes,相信每个公司做项目都会封装一个接口日志的切面,每一个客户端请求进来,怎么知道是哪一个接口路由? 一般都是通过 RequestContextHolder.getRequestAttributes()获取请求的基本信息,而每一个request 在 RequestContextHolder 中就是通过TreadLocal 去缓存的 image.png

2.还有比较常见的就是分布式事务框架,比如seata,生成一个全局的事务ID ,保存在TreadLocal中,然后去执行目标方法,执行完了再从TradLocal中取回事务ID传递给接口.

面试题: 保证线程安全的方式有很多,为什么要用TreadLocal呢?

因为不会阻塞,像Java中的锁机制,大都会阻塞线程,而ThreadLocal 则是采用空间换时间的方式,操作只针对当前线程,所以不存在阻塞情况,但是会消耗内存,因为要为每个线程去分配内存去缓存数据.

看下一个问题,也是道常见面试题,ThreadLocal为什么会造成内存泄漏?

一般人简单的回答下就是因为弱引用(思考一下为什么不用强引用?)的关系,导致GC无法回收缓存的对象,一直占用内存.这样答实际上是拿不到分的,最好回答的详细一点,例如:ThreadLocal是存在于堆中的,每一个线程持有一个ThreadLocalMap,ThreadLocalMap 内部是一个Entry数组(思考一下为什么是一个数组?等下会分析一波),entry的key指向ThreadLocal 的引用,value是我们要缓存的值,当堆中的TreadLocal 为null时,如果触发了GC,GC线程会回收掉TreadLocal ,Entry的KEY指向的引用变为null ,但是Value还存在,导致GC无法回收,一直占用内存不释放,造成内存泄露.

如何避免内存泄漏?

这个倒是没啥可说的,每次使用完TreadLocal记得调用一下它的remove方法就可以了.其实还有一个比较骚的操作,可以利用Java的反射机制去获取当前线程的ThreadLocalMap,手动移除,但是不推荐啊.

回过头来,看一下刚才提出的两个问题.

第一个问题,TreadLocal为什么采用弱引用而不是强引用?这里先了解一下强弱引用的概念

强引用:被引用关联的对象永远不会被GC回收.

弱引用:被引用关联的对象,当发生GC时都会被回收掉,清理内存.

所以答案比较明显,如果用强引用的情况下,加入内存使用率已经100%了,再去申请内存就会OOM了,所以不用强引用是避免内存溢出.

第二个问题,为什么ThreadLocalMap中的table是一个Entry数组?

其实很简单,Entry对象的Key指向的是TreadLocal的引用,工作中一个场景,要对每一个请求的request进行header和token的鉴权,就分别定义了不同的TreadLocal,所以要用数组存