关于ThreadLocal的这两个派生类你了解多少?

1,582 阅读7分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

ThreadLocal

在JDK1.2中提供了java.lang.ThreadLocal,顾名思义:线程本地变量,即ThreadLocal为多线程访问变量提供了一种新的不需要同步加锁就能保证线程安全的方式,使用ThreadLocal时,每个线程都持有变量的副本,因而不会产生多线程并发问题,简单看下ThreadLocal的使用:

public class ThreadLocalTest {
    private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(3);

    private static final ThreadLocal<String> USER_THREAD_LOCAL = new ThreadLocal<>();

    @Test
    public void threadLocalTest() throws InterruptedException {

        EXECUTOR_SERVICE.submit(() -> {
            //请求进来时,初始化用户信息
            USER_THREAD_LOCAL.set("李四");
            System.out.println(Thread.currentThread().getName() + "当前登陆用户:" + USER_THREAD_LOCAL.get());
        });

        EXECUTOR_SERVICE.submit(() -> {
            //请求进来时,初始化用户信息
            USER_THREAD_LOCAL.set("张三");
            System.out.println(Thread.currentThread().getName() + "当前登陆用户:" + USER_THREAD_LOCAL.get());
        });
    }
}

输出:
pool-1-thread-1当前登陆用户:李四
pool-1-thread-2当前登陆用户:张三

这个例子中,假如我们通过ThreadLocal来存储用户信息,因为对于常见的后台应用中,每个请求到来都可以标识唯一一个用户,所以这种场景,我们就可以将用户信息放在本地变量中,从而不用每次需要使用用户信息都要去查库,或者将用户信息通过参数层层传递。

但是现在问题来了,假如说有这种场景:用户A请求进来,然后呢接下来需要异步给用户A处理其他的事情,比如发短信,那么再来看ThreadLocal能否为我们满足:

public class ThreadLocalTest {
    private static final ExecutorService EXECUTOR_SERVICE = Executors.newFixedThreadPool(3);

    private static final ExecutorService PUSH_EXECTOR_SERVICE = Executors.newFixedThreadPool(3);

    private static final ThreadLocal<String> USER_THREAD_LOCAL = new ThreadLocal<>();

    @Test
    public void threadLocalTest002() {

        EXECUTOR_SERVICE.submit(() -> {
            //请求进来时,初始化用户信息
            USER_THREAD_LOCAL.set("李四");
            System.out.println(Thread.currentThread().getName() + "当前登陆用户:" + USER_THREAD_LOCAL.get());

            // 接着异步发送短信
            System.out.println("开始异步发送短信");
            PUSH_EXECTOR_SERVICE.submit(() -> {
                System.out.println(Thread.currentThread().getName() + "发送短信给:" + USER_THREAD_LOCAL.get());
            });
        });

    }
}

输出:
pool-1-thread-1当前登陆用户:李四
开始异步发送短信
pool-2-thread-1发送短信给:null

从输出可以看到,发送短信时,并未获取到用户信息,其实根据ThreadLocal的定义和使用场景,预期的确如此,因为不同的线程ThreadLocal的值都是需要自身去初始化的,然而,我们这种场景其实想让用户信息跟随着带下去的,因为对于用户A的请求来说,在本次请求中新开线程去处理其他事情肯定也是当前用户的事情,于是我们的代码中可能出现如下写法:

    EXECUTOR_SERVICE.submit(() -> {
        //请求进来时,初始化用户信息
        USER_THREAD_LOCAL.set("李四");
        System.out.println(Thread.currentThread().getName() + "当前登陆用户:" + USER_THREAD_LOCAL.get());

        // 接着异步发送短信
        System.out.println("开始异步发送短信");

        // 将当前用户信息取出来
        String currentUser = USER_THREAD_LOCAL.get();
        PUSH_EXECTOR_SERVICE.submit(() -> {
            // 设置用户上下文
            USER_THREAD_LOCAL.set(currentUser);
            System.out.println(Thread.currentThread().getName() + "发送短信给:" + USER_THREAD_LOCAL.get());
        });
    });

输出:
pool-1-thread-1当前登陆用户:李四
开始异步发送短信
pool-2-thread-1发送短信给:李四
}

这样就解决了我们的问题,看起来不错,但是假如这种情况的本地变量很多呢?假如需要这么用的地方也很多呢?我不止要异步发短信,还要异步发邮件等等.....,然后是不是我们的代码中就充满了这种从原始线程取出本地变量,再设置到新线程样板代码,这看起来好像不是很优雅的样子,作为一个程序员,得有工匠精神,这不能忍!

于是乎,InheritableThreadLocal诞生了!

InheritableThreadLocal

其实在第一节中,通过举例大概了解到了ThreadLocal的不足:即某些场景我们希望线程的本地变量可以被继承! 再说一个例子:那就是我们现在分布式系统中肯定都会接入调用链追踪的功能,我们希望通过一个traceId,可以将一个请求经过的地方从头到尾关联起来,这个时候如果本地变量可以被继承,那就算是期间发生异步调用,由于新线程继承了父线程的变量,所以依然可以通过一个tranceId串联起来,此时糖葫芦的那根棍就是这个traceId,糖葫芦的每个糖球就是我们每个异步线程!

image.png

但是如果只有ThreadLocal,那我们还是需要在每个地方手动进行设置,样板重复代码不说,很容易遗漏,所以InheritableThreadLocal就是为我们解决这个问题。

Inheritable意为可继承的说白点就是当父线程创建子线程的时候,子线程会把父线程的所有本地变量再copy一份给自己! InheritableThreadLocal继承自ThreadLocal,并且覆写了如下三个方法:

  • protected T childValue(T parentValue)
  • ThreadLocalMap getMap(Thread t)
  • void createMap(Thread t, T firstValue)

在介绍这三个方法之前,先来看下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;

可以看到在线程Thread中分别持有两个ThreadLocalMap(ThreadLocalMap牵扯到ThreadLocal的原理,不懂的先出门左转再右转再直行看完ThreadLocal原理在掉头左转再右转回来),其中一个是为ThreadLocal使用的(创建的子线程进行不继承),一个是为InheritableThreadLocal使用的(创建子线程时会将这里的变量都继承到子线程的inheritableThreadLocals中)。有了这个前景知识,再来看看InheritableThreadLocal重写的方法逻辑:

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

懂了吧?其实就是将创建和获取本地变量都指向Thread.inheritableThreadLocals而已!

那还有一个方法呢?

protected T childValue(T parentValue) {
    return parentValue;
}

这个方法会在创建子线程时,复制父线程的inheritableThreadLocals会用到,传入父线程变量,返回子线程变量,可以看到在InheritableThreadLocal的默认实现里,是原封不懂将父线程变量传递到子线程的,这意味啥?意味多个线程同时共享这个变量呀,所以这里是一个注意点,我们想子线程在继承时复制父线程的变量,那我们也可以自己搞一个继承类,然后在这个方法中返回copy对象等逻辑!

讲了这么多,接下来实战一下吧

private static final ThreadLocal<String> USER_Inheritable_THREAD_LOCAL = new InheritableThreadLocal<>();

@Test
public void inheritableThreadLocalTest002() {
    EXECUTOR_SERVICE.submit(() -> {
        //请求进来时,初始化用户信息
        USER_Inheritable_THREAD_LOCAL.set("李四");
        System.out.println(Thread.currentThread().getName() + "当前登陆用户:" + USER_Inheritable_THREAD_LOCAL.get());

        // 接着异步发送短信
        System.out.println("开始异步发送短信");
        
        PUSH_EXECTOR_SERVICE.submit(() -> {
            // 设置用户上下文
            System.out.println(Thread.currentThread().getName() + "发送短信给:" + USER_Inheritable_THREAD_LOCAL.get());
        });
    });

}

输出:
pool-1-thread-1当前登陆用户:李四
开始异步发送短信
pool-2-thread-1发送短信给:李四

至于在创建子线程时是怎么继承的看一下java.lang.Thread#init(java.lang.ThreadGroup, java.lang.Runnable, java.lang.String, long, java.security.AccessControlContext, boolean)的实现就可以了

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

看到这,就完了吗?不不不,你想多了,不知道你有没有意识到一个问题,大家都用线程池对吧?处理请求的线程也是来自线程池对吧?线程池意味啥?意味着线程会被复用,复用意味着啥?用户A请求进来啪啪啪一顿操作,然后相关线程写入了用户A相关信息,那用户B进来了,用户B有点惨只能用人家用过的线程, 所以有没有感觉?对啊,有可能发生线程污染问题,也就是我们俗称的线程信息串了!

于是乎,TransmittableThreadLocal大哥闪亮登场!

image.png

TransmittableThreadLocal

首先说一下,TransmittableThreadLocal并不是JDK中的,而是阿里开源类库,开源地址:github.com/alibaba/tra…

看下官网介绍:TransmittableThreadLocal(TTL):在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。一个Java标准库本应为框架/中间件设施开发提供的标配能力,本库功能聚焦 & 0依赖,支持Java 17/16/15/14/13/12/11/10/9/8/7/6。

JDKInheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

下面是几个典型场景例子。

  1. 分布式跟踪系统 或 全链路压测(即链路打标)
  2. 日志收集记录系统上下文
  3. SessionCache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

教程的话,我相信人家官网讲的也是十分清楚了,到这里咱们就暂且结束!