增强版的ThreadLocal-TransmittableThreadLocal

3,052 阅读4分钟

一、前言

ThreadLocal是JDK里面提供的一个thread-local(线程局部)的变量,当一个变量被声明为ThreadLocal时候,每个线程会持有该变量的一个独有副本;但是ThreadLocal不支持继承性,虽然JDK里面提供了InheritableThreadLocal来解决继承性问题,但是其也是不彻底的,本节我们谈谈增强的TransmittableThreadLocal,其可以很好解决线程池情况下继承问题。

二、TransmittableThreadLocal

前面说了,当一个变量被声明为ThreadLocal时候,每个线程会持有该变量的一个独有副本,比如下面例子:

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

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

        new Thread(() -> {
            try {
                // 设置本线程变量
                parent.set(Thread.currentThread().getName() + "hello,jiaduo");

                // dosomething
                Thread.sleep(3000);

                // 使用线程变量
                System.out.println(Thread.currentThread().getName() + ":" + parent.get());

                // 清除
                parent.remove();
                
                // do other thing
                //.....

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "thread-1").start();

        new Thread(() -> {
            try {
                // 设置本线程变量
                parent.set(Thread.currentThread().getName() + "hello,jiaduo");

                // dosomething
                Thread.sleep(3000);

                // 使用线程变量
                System.out.println(Thread.currentThread().getName() + ":" + parent.get());

                // 清除
                parent.remove();
                
                // do other thing
                //.....
                
            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "thread-2").start();
}

如上代码线程1和线程2各自持有parent变量中的副本,其相互之间并发访问自己的副本变量,不会存在线程安全问题。

但是ThreadLocal不支持继承性:

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

        ThreadLocal<String> parent = new ThreadLocal<String>();
        parent.set(Thread.currentThread().getName() + "hello,jiaduo");

        new Thread(() -> {

            try {
                
                // 使用线程变量
                System.out.println(Thread.currentThread().getName() + ":" + parent.get());

                // do other thing
                // .....

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "child-thread").start();
}

如上代码main线程内设置了线程变量,然后在main线程内开启了子线程child-thread,然后在子线程内访问了线程变量,运行会输出:child-thread:null;也就是子线程访问不了父线程设置的线程变量;JDK中InheritableThreadLocal可以解决这个问题:

        InheritableThreadLocal<String> parent = new InheritableThreadLocal<String>();
        parent.set(Thread.currentThread().getName() + "hello,jiaduo");

        new Thread(() -> {

            try {
                
                // 使用线程变量
                System.out.println(Thread.currentThread().getName() + ":" + parent.get());

                // do other thing
                // .....

            } catch (Exception e) {
                e.printStackTrace();
            }
        }, "child-thread").start();

运行代码会输出:child-thread:mainhello,jiaduo,可知InheritableThreadLocal支持继承性。但是InheritableThreadLocal的继承性是在new Thread创建子线程时候在构造函数内把父线程内线程变量拷贝到子线程内部的(可以参考《Java并发编程之美》一书),而线上环境我们很少亲自new线程,而是使用线程池来达到线程复用,线上环境一般是把异步任务投递到线程池内执行;所以父线程向线程池内投递任务时候,可能线程池内线程已经创建完毕了,所以InheritableThreadLocal就起不到作用了,例如下面例子:

    // 0.创建线程池
    private static final ThreadPoolExecutor bizPoolExecutor = new ThreadPoolExecutor(2, 2, 1, TimeUnit.MINUTES,
            new LinkedBlockingQueue<>(1));

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

        // 1 创建线程变量
        InheritableThreadLocal<String> parent = new InheritableThreadLocal<String>();

        // 2 投递三个任务
        for (int i = 0; i < 3; ++i) {
            bizPoolExecutor.execute(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });

        }

        // 3休眠4s
        Thread.sleep(4000);

        // 4.设置线程变量
        parent.set("value-set-in-parent");

        // 5. 提交任务到线程池
        bizPoolExecutor.execute(() -> {
            try {
                // 5.1访问线程变量
                System.out.println("parent:" + parent.get());
            } catch (Exception e) {
                e.printStackTrace();
            }

        });
}

如上代码2向线程池投递3任务,这时候线程池内2个核心线程会被创建,并且队列里面有1个元素。然后代码3休眠4s,旨在让线程池避免饱和执行拒绝策略,然后代码4设置线程变量,代码5提交任务到线程池。运行输出:parent:null,可知子线程内访问不到父线程设置变量。

下面我们使用TransmittableThreadLocal修改代码如下:

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

        // 1 创建线程变量
        TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();

        // 2 投递三个任务
        for (int i = 0; i < 3; ++i) {
            bizPoolExecutor.execute(() -> {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            });

        }

        // 3休眠4s
        Thread.sleep(4000);

        // 4.设置线程变量
        parent.set("value-set-in-parent");

        // 5. 提交任务到线程池
        Runnable task = () -> {
            try {
                // 5.1访问线程变量
                System.out.println("parent:" + parent.get());
            } catch (Exception e) {
                e.printStackTrace();
            }

        };
        
        // 额外的处理,生成修饰了的对象ttlRunnable
        Runnable ttlRunnable = TtlRunnable.get(task);
        
        bizPoolExecutor.execute(ttlRunnable);
}

如上代码5我们把具体任务使用TtlRunnable.get(task)包装了下,然后在提交到线程池,运行代码,输出:parent:value-set-in-parent,可知子线程访问到了父线程的线程变量

三、总结

TransmittableThreadLocal完美解决了线程变量继承问题,其是淘宝技术部 哲良开源的一个库,github地址为:github.com/alibaba/tra…,后面我们会探讨其内部实现原理,敬请期待。

file