ThreadLocal 分析与实现

发布时间 2023-08-02 22:15:09作者: Geraltz'Rivia

在java 8的实现中,注释是这么描述ThreadLocal类的

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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).

这个类提供了线程本地变量。这些变量与普通变量不同,每个访问它们的线程(通过其get或set方法)都有自己独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程关联的类中的私有静态字段(例如,用户ID或事务ID)。

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

只要线程存活且ThreadLocal实例可访问,每个线程都持有对其线程本地变量副本的隐式引用;当线程消失时,其所有线程本地实例的副本都可能会被垃圾回收(除非存在对这些副本的其他引用)。

ThreadLocal可以理解为一个共有的变量,但是这个变量的值对于每个thread来说都是独立的

ThreadLocal示例

除了main主线程,还有使用线程池新建的两个线程,这两个线程使用ThreadLocal设置了一个变量threadLocal

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalDemo {

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

    public static void testTL() throws InterruptedException {
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        int count = 2;
        ExecutorService executor = Executors.newFixedThreadPool(count);
        for (int i = 0; i < count; i++) {
            executor.execute(() -> threadLocal.set(Thread.currentThread().getName()));
        }
        for (int i = 0; i < count; i++) {
            executor.execute(() -> System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get()));
        }
        Thread.sleep(10);
        System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
        executor.shutdown();
    }
}

运行结果:

pool-1-thread-1 : pool-1-thread-1
pool-1-thread-2 : pool-1-thread-2
main : null

main中没有设置threadlocal变量,因此为null

ThreadLocal实现

ThreadLocal类的依靠内部类ThreadLocalMap与Thread类

ThreadLocalMap先理解为一个类似HashMap的结构,key是threadlocal变量本身,value是对应的值;

Thread类中有个field threadLocals ,类型就是ThreadLocalMap,ThreadLocal类提供一个方法createMap,可以初始化thread类中的这个成员

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

观察ThreadLocal类的get与set方法

    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 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;
    }

都是通过getMap方法拿到thread对象的threadLocals,然后再通过对map的get和set方法,进行变量的获取或者保存。

ThreadLocalMap数据结构

ThreadLocalMap内部定义了Entry的数据结构, Entry使用 ThreadLocal 作为 key、具体变量使用 value 保存,同时 Entry 继承了 WeakReference,当没有强引用指向 ThreadLocal 对象时,key 会被回收。

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

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

Map内部是一个Entry数组,同时有信息记录数组大小,扩容阈值信息

Map数据结构的set、get、remove等方法可以阅读源码查看。

InheritableThreadLocal

ThreadLocal不能把变量传递给子线程,java 提供了InheritableThreadLocal来进行threadlocal的传递

public class InheritThreadLocalDemo {

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

    public static void testITL() throws InterruptedException {
        ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
        threadLocal.set(Thread.currentThread().getName());
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get()));
        Thread.sleep(10);
        System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
        executor.shutdown();
    }
}

运行结果:

pool-1-thread-1 : main
main : main

实现

Thread类的成员除了threadLocals还有inheritableThreadLocals,后者是实现InheritableThreadLocal的关键,重写了getMap方法

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

在Thread的init方法中,对于inheritableThreadLocals进行了处理

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

createInheritedMap传递了当前线程的inheritableThreadLocals,调用了ThreadLocalMap(ThreadLocalMap parentMap)方法new一个ThreadLocalMap,这个函数调用了Object value = key.childValue(e.value);

childValue是可以被覆写的方法,默认实现是使用父线程的threadlocal的引用

注意,InheritableThreadLocal在线程池下是无效的,原因是只有在创建Thread时才会去复制父线程存放在InheritableThreadLocal中的值,而线程池场景下,主业务线程仅仅是将提交任务到任务队列中。

ThreadLocal内存泄露

学习阅读其他人写的关于threadlocal的用法,都提到了容易引发内存泄露问题;在上面对ThreadLocalMap数据结构的分析中可以知道,如果只清除了threadlocal变量,weakreference的key被回收,但是value是强引用无法回收,导致内存泄露;

因此在不使用threadlocal的时候需要使用remove方法清除value的引用

其他ThreadLocal实现

阿里开源的 TransmittableThreadLocal 实现,可以解决 InheritableThreadLocal 在线程池场景下无效的问题,它可以讲threadlocal从线程创建时设置改为任务提交时设置

github地址 https://github.com/alibaba/transmittable-thread-local

网上有很多分析使用的文章,这里不贴了,值得注意的是这个 TTL 库支持以agent方式启动,可以使用java-agent机制修饰运行中的线程池