美文网首页javaapm
ThreadLocal及其扩展

ThreadLocal及其扩展

作者: 修行者12138 | 来源:发表于2024-04-24 23:09 被阅读0次

    ThreadLocal使用

    用于相同线程内上下文的传递,避免显式传参,简化代码。
    比如controller层把用户信息set到ThreadLocal,在service层get获取,无需显式传参。

    ThreadLocal原理

    Thread类中有一个threadLocals变量,类型为ThreadLocalMap

    public class Thread {
        /* ThreadLocal values pertaining to this thread. This map is maintained
         * by the ThreadLocal class. */
        ThreadLocal.ThreadLocalMap threadLocals = null;
    }
    

    ThreadLocalMap是一个自定义的hash map,底层是Entry数组,提供了getEntry(ThreadLocal<?> key) 、set(ThreadLocal<?> key, Object value)等api,使用线性探测法处理hash冲突。
    从api可以看出,ThreadLocalMap的key是ThreadLocal对象,value是泛型对象值。

    ThreadLocal的值,存储在Thread对象实例的堆内存空间里,而不是ThreadLocal对象,ThreadLocal只是提供了get set方法用于维护变量值。

    InheritableThreadLocal使用

    Thread类中,除了有threadLocals变量,还有一个inheritableThreadLocals变量

    public class Thread {
        /* ThreadLocal values pertaining to this thread. This map is maintained
         * by the ThreadLocal class. */
        ThreadLocal.ThreadLocalMap threadLocals = null;
        
        /*
         * InheritableThreadLocal values pertaining to this thread. This map is
         * maintained by the InheritableThreadLocal class.
         */
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    }
    

    假设这样一个场景: 父线程开了一个子线程,但是我们希望在子线程中可以访问父线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new ThreadLocal();
    
        for (int i = 0; i < 10; i++) {
            // 父线程set
            threadLocal.set(i);
    
            new Thread(() -> {
                // 子线程get
                System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
            }).start();
        }
    }
    

    执行结果

    Thread-0: null
    Thread-2: null
    Thread-1: null
    Thread-4: null
    Thread-3: null
    Thread-5: null
    Thread-6: null
    Thread-7: null
    Thread-8: null
    Thread-9: null
    

    如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal

    public static void main(String[] args) {
        ThreadLocal<Integer> threadLocal = new InheritableThreadLocal();
    
        for (int i = 0; i < 10; i++) {
            // 父线程set
            threadLocal.set(i);
    
            new Thread(() -> {
                // 子线程get
                System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
            }).start();
        }
    }
    

    再次执行

    Thread-0: 0
    Thread-2: 2
    Thread-1: 1
    Thread-3: 3
    Thread-4: 4
    Thread-5: 5
    Thread-6: 6
    Thread-7: 7
    Thread-8: 8
    Thread-9: 9
    

    可以看到,每个子线程都可以访问到从父线程传递过来的一个数据。

    InheritableThreadLocal原理

    InheritableThreadLocal源码如下,可以看到InheritableThreadLocal重写了父类的几个方法。

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

    重写createMap方法是为了把变量存到Thread里的inheritableThreadLocals变量,而不是threadLocals变量。

    Thread类的构造方法,会调用init方法。
    调用子线程Thread类的init方法时,如果parent.inheritableThreadLocals不为空,会把parent.inheritableThreadLocals浅拷贝到子线程的inheritableThreadLocals.

    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    }
    

    实际业务里,InheritableThreadLocal的使用场景很少,主要原因如下

    1. 上下文的拷贝是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不行了,因为线程池的核心线程是提前创建的
    2. 父子线程的map里的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题(父子线程同时访问)

    TransmittableThreadLocal原理

    阿里TransmittableThreadLocal依赖

    <!-- https://mvnrepository.com/artifact/com.alibaba/transmittable-thread-local -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>transmittable-thread-local</artifactId>
        <version>2.14.5</version>
    </dependency>
    

    如果使用线程池,有3个时机需要考虑: 创建子线程、提交任务、执行任务

    1. 线程池的核心线程在线程池初始化时就创建了,创建时机远远早于用户请求。
    2. 提交任务后,不一定会马上执行,有可能核心线程数用完了,需要在阻塞队列里等待。
    3. 最好是在执行任务前,把父线程的上下文拷贝到子线程。

    假设一个请求打到tomcat,tomcat从线程池里取出线程A(父线程),在controller层设置了用户信息到线程A的ThreadLocal。然后进入service层,使用线程池,从线程池里拿出线程B执行业务逻辑。

    线程A提交任务后,就可以处理别的请求了。

    如果等到线程B执行任务前,才从线程A拷贝上下文,这时候线程A的ThreadLocal可能被其他请求的数据覆盖了。

    因此,线程A提交任务时,就需要把上下文拷贝到一个地方,然后线程B执行任务前,再从那个地方拷贝上下文。

    总结

    ThreadLocal: 无法复制上下文至子线程中
    InheritableThreadLocal: 创建子线程时,从父线程复制上下文
    TransmittableThreadLocal: 子线程执行任务时,从父线程复制上下文

    相关文章

      网友评论

        本文标题:ThreadLocal及其扩展

        本文链接:https://www.haomeiwen.com/subject/ahngxjtx.html