头图

引言

正如文章标题,本文重点在于剖析ThreadLocal的源码,先对ThreadLocal下定义

ThreadLocal是线程级别的私有变量

即使你没有使用过ThreadLocal也可以阅读,本文会从ThreadLocal最基本的使用入手,结合源码及图片由浅入深地分享我在学习ThreadLocal源码中的收获和理解,希望对你有帮助.

一、初识ThreadLocalMap

1.1 ThreadLocal的使用

ThreadLocal<String> traceId = new ThreadLocal<>()
//ThreadLocal<String> traceId = ThreadLocal.withInitial(()->"initial");
...
 traceId.set(UUID.randomUUID().toString);
 ...
 traceId.get();
 ...
 traceId.remove()

这是一个ThreadLocal精简后的传统使用场景,在过去的web应用里,请求到达服务器以后可以分配一个traceId用于追踪请求到响应的链路,或者存储单个请求内的用户身份信息等等.尽管最终执行的时候分发到了不同的线程内,每个线程内可见的数据依旧是自己的用户身份信息或者traceId,在当前线程内的所有地方都可以通过ThreadLocal实例.get()的方式获取到这个信息.得益于ThreadLocal的线程隔离性,无需担心当前线程会取到其他线程的结果.

之所以说是在过去的web应用,是因为在当下一条请求到达服务器以后几乎一定会经历异步执行或途经多个微服务,这时ThreadLocal作为线程隔离的变量就不够用了,无法把一条链路中所需的信息传递下去.但这并不代表ThreadLocal就无用武之地了,恰恰相反,JDK原生的InheritableThreadLocal(简称ITL,先眼熟,后文还会见到),用于跨线程传递的组件MDC和阿里开源的TransmittableThreadLocal(简称TTL)都是基于ThreadLocal实现的,这些仅仅是冰山一角.至于如何基于ThreadLocal做到上述的扩展,我之后也会抽空更新对于MDC和TTL的分享.

我在注释中补充了ThreadLocal除了最常用的set(),get()和remove()以外,提供的public方法withInitial,它允许通过lambda表达式的方式给一个ThreadLocal赋予初始值.

1.2 剥开ThreadLocal的外壳

ThreadLocal对外暴露的public方法实际上就只有这么多,你可以直接把ThreadLocal当成一个线程隔离的容器,通过set()和get()进行存取.但如果在工作和面试中有人问你为什么ThreadLocal具备线程隔离性,那只会使用就不够了

首先我们来看Thread内部的两个私有变量

ThreadLocal.ThreadLocalMap threadLocals = null;
//1.1中提到的ITL
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

也就是说每一个线程内部实际上都持有一个ThreadLocalMap,它是ThreadLocal的静态内部类,存放着当前线程内的所有ThreadLocal实例.我们再继续往下看ThreadLocalMap内部的结构

static class ThreadLocalMap {  

static class Entry extends WeakReference<ThreadLocal<?>> {  
    /** The value associated with this ThreadLocal. */  
    Object value;  

    Entry(ThreadLocal<?> k, Object v) {  
        super(k);  
        value = v;  
    }  
}  

private static final int INITIAL_CAPACITY = 16;  

private Entry[] table;

private int size = 0;  

private int threshold;

ThreadLocalMap本质上是一个用Entry[]数组实现的简易哈希表,其中每一个Entry继承了弱引用,Entry的key存放ThreadLocal,value存放值;

除此之外,我还节选了一部分参数,比如这个哈希表的初始容量是16,初始大小是0,用于控制哈希表扩容的阈值等等.提到哈希表的实现,就一定绕不开如何处理哈希碰撞等细节,但我认为目前为止对于学习ThreadLocal是如何实现线程隔离的已经足够了,感兴趣的话我会单独出一篇文章介绍ThreadLocalMap内部是如何实现的.

ThreadLocal静态结构

结合这张图,我们再以ThreadLocal.set() 和 get()来回看ThreadLocal中提供的方法内部是如何实现的:

public void set(T value) {  
    Thread t = Thread.currentThread();  
    ThreadLocalMap map = getMap(t);  
    if (map != null) {  
        map.set(this, value);  
    } else {  
        createMap(t, value);  
    }  
}

ThreadLocalMap getMap(Thread t) {  
      return t.threadLocals;  
}   

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;  
        }  
    }  
    //空实现,返回null    
    return setInitialValue();  
}

可以看到,不管是set()还是get(),都会首先获取当前线程,并拿到当前线程下的ThreadLocalMap,再调用map内部的set()和get()方法,我们再来看ThreadLocalMap内部的set()和get()是如何实现的

//set核心代码
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;  
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    ...
    e.value = value;
    return;
}

//get核心代码
private Entry getEntry(ThreadLocal<?> key) {  
    int i = key.threadLocalHashCode & (table.length - 1);  
    Entry e = table[i];  
    return e;
}

可以看到,在set()和get()的时候都会根据ThreadLocal实例内部的hashcode和哈希掩码做与运算来计算哈希槽的下标,直接修改或返回ThreadLocalMap中的value即可.至此为止,ThreadLocal的面纱就已经被彻底揭开:ThreadLocal只是暴露给使用者的实例,自身并没有存储值,而是在ThreadLocalMap内部某一个Entry的value里,找到这个value的方式就是通过ThreadLocal计算得到哈希槽下标.

补充一句,在set()方法中我们可以看到ThreadLocalMap实际上采用了延迟加载,ThreadLocal实例没有赋值时指向的ThreadLocalMap是null,只有当有set()操作发生时才会初始化ThreadLocalMap,代码如下

void createMap(Thread t, T firstValue) {  
    t.threadLocals = new ThreadLocalMap(this, firstValue);  
}

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {  
    table = new Entry[INITIAL_CAPACITY];  
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  
    table[i] = new Entry(firstKey, firstValue);  
    size = 1;  
    setThreshold(INITIAL_CAPACITY);  
}
我之所以在set()和get()的代码块中注释了核心代码,就是因为还隐藏了许多ThreadLocalMap内部的细节,譬如如果发生了哈希碰撞怎么处理,以及和标记和清除过期哈希槽相关的逻辑

二、使用时的注意事项

2.1 典中典之内存泄漏

这个问题在面试过程中可以说是必背的八股,但未必每个人都真正理解了背后的含义,这和JVM运行时的内存结构,引用级别和GC息息相关.通过上一章的内容你已经可以对ThreadLocal如何实现线程隔离侃侃而谈,那么这一小节旨在帮你真正理解为什么会产生内存泄露的问题,以及如何在使用过程中避免内存泄漏.

废话不多说,直接上图:

运行时JVM示意图

如果你对这张图中出现的JVM内存结构或引用级别还不够了解,就无法真正理解老生常谈的内存泄漏问题,这部分网上的资料非常多也很全面

2.1.1 为什么Entry的key采用弱引用

前面我们说过ThreadLocal自身并不存储值,它是暴露给使用者的,本质上还是在操作ThreadLocalMap.不妨想想如果某个ThreadLocal的实例在程序中被设为null,Entry的key作为强引用时会发生什么,这显然会导致内存泄漏,这是因为:

对使用者而言指向堆上ThreadLocal对象的引用就只有自己的ThreadLocal实例,但通过前面的学习我们知道实际上还有一份引用存在于Entry的key里,所以key只能是弱引用,否则ThreadLocal在堆上的对象实例就永远不会被回收.

因此,每次GC的时候都会回收堆上只有弱引用指向的ThreadLocal对象

2.1.2 怎么还有内存泄漏?

Entry的key通过设置为弱引用解决了,但value显然行不通,它只能是强引用.所以面试八股中老生常谈的内存泄漏问题指的是当key被回收设置为null以后,value的强引用依然存在.

为了应对这种情况,ThreadLocalMap的get(),set()和remove()方法中均提供了对ThreadLocalMap中key为null时删除对应value的操作.

不同之处在于:

  • remove()一定会抹除threadLocal对应的value
  • set()和get()方法只是在一些比如哈希未命中需要额外做处理的时候顺带回收遍历过程中key为null的value,如果第一次直接命中就直接返回,无法清理

因此,最佳实践就是不再使用ThreadLocal实例时显示调用remove()

2.2 数据污染

线程池内部如果使用了ThreadLocal要格外注意,因为线程池内线程会被复用,一定要及时remove(),避免造成数据污染

2.3 线程隔离等于线程安全吗?

直接说结论,不等于.当ThreadLocal中的value是引用类型时,比如MDC中的Map<String,String>.这种情况下要么明确使用场景确保没有通过本地引用修改value,要么返回引用时采用深拷贝.

三、浅析InheritableThreadLocal

在引言中提到了线程级别的ThreadLocal已经不能满足当下跨线程的需要,ITL就是JDK原生的支持父子线程间携带信息的扩展,代码非常简短:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {  

    protected T childValue(T parentValue) {  
        return parentValue;  
    }  
  
    ThreadLocalMap getMap(Thread t) {  
       return t.inheritableThreadLocals;  
    }  

    void createMap(Thread t, T firstValue) {  
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);  
    }  
}

直接继承自ThreadLocal并重写了三个方法,第一个方法决定了父子线程传递值的方式是直接传递,后两个方法仅仅把创建和获取ThreadLocalMap的实例设置为ITL独立的map实例.如果你回看1.2中Thread的代码,你会发现ITL已经声明了自己的ThreadLocalMap.

那怎么使用呢?实际上你只需要声明一个ITL,然后像ThreadLocal一样使用就可以了,父子线程的传递是自动完成的,怎么做到的?

if (inheritThreadLocals && parent.inheritableThreadLocals != null)  
this.inheritableThreadLocals =  
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

这段代码来自于Thread内private的构造方法,可以看到只要当前线程的inheritThreadLocals布尔值为真且父线程的ITL是有值的,子线程会接受createInheritedMap()的结果,这个方法的内部就是对父线程的ITL做了深拷贝并返回.那问题又来了,大多都是直接new Thread()再重写里面的run()方法的,我怎么知道这个布尔值是多少?只要再往上追一层Thread对外共开的无参构造方法就水落石出了:

public Thread(Runnable target) {  
    this(null, target, "Thread-" + nextThreadNum(), 0);  
}

public Thread(ThreadGroup group, Runnable target, String name,  
              long stackSize) {  
    this(group, target, name, stackSize, null, true);  
}

可以看到,这个布尔值是默认为true的,这也是为什么前面说父子线程的传递是自动完成的.

实际上ITL对ThreadLocal做的扩展依旧不够,当下绝大部分都是采用线程池复用的方式执行异步任务,而不是简单的父子线程.这就需要类似MDC和TTL这样的技术了,值得一提的是,它们的实现方式依然是对ThreadLocal做封装.其中MDC的源码实际上非常简短,而阿里的TTL拥有非常友好和活跃的社区氛围,推荐直接在官方文档处学习:alibaba/transmittable-thread-local: 📌 a missing Java std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components. (github.com)

四、总结

本文结合了部分ThreadLocal的源码 以及 静态代码和JVM运行时的图示,主要讲解了以下几点:

1.ThreadLocal如何做到线程隔离

每个Thread下持有一个ThreadLocalMap,ThreadLocal以弱引用key的方式存储在ThreadLocalMap中,真正的值存储在value.

2.ThreadLocal使用时的注意事项

  • 显式调用remove()防止内存泄漏
  • 需要考虑到线程复用的情况,避免数据污染
  • 线程隔离≠线程安全,对于引用类型要格外敏感

3.ITL如何做到父子线程传递
在new Thread()内部自动拷贝父线程的ITL

五、todo

对于ThreadLocal源码的解析中隐藏的ThreadLocalMap内部对简易哈希表的实现,以及没有展开讲的MDC和TTL这样的跨线程传递方案,后续会抽时间梳理并分享.

水平有限,文章难免存在谬误和不完善的地方,随时欢迎批评指正,补充或者分享自己对于ThreadLocal设计的看法.


Andy_Shawshank
1 声望0 粉丝