ThreadLocal的进化——TransmittableThreadLocal

上一篇文章中,咱们谈到了 InheritableThreadLocal,它解决了 ThreadLocal 针对父子线程没法共享上下文的问题。但咱们可能据说过阿里的开源产品TransmittableThreadLocal,那么它又是作什么的呢? git

线程池中的共享

咱们在多线程中,不多会直接 new 一个线程,更多的多是利用线程池处理任务,那么利用 InheritableThreadLocal 能够将生成任务线程的上下文传递给执行任务的线程吗?废话很少说,直接上代码测试一下:github

public class InheritableThreadLocalContext {

    private static InheritableThreadLocal<Context> context = new InheritableThreadLocal<>();

    static class Context {

        String name;

        int value;
    }

    public static void main(String[] args) {
        // 固定线程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任务的线程对context进行赋值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        InheritableThreadLocalContext.context.set(contextMain);
                        // 提交任务
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            executorService.execute(() -> {
                                // 执行任务的子线程
                                Context contextChild = InheritableThreadLocalContext.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            });
                        }

                    }
            ).start();
        }
    }
}复制代码

咱们但愿的结果是,子线程输出的内容可以和父线程对应上。然而,实际的结果却出乎所料,我将结果整理一下:多线程

Thread1 produce task 21
// 省略8行
Thread1 produce task 30

Thread2 produce task 41
// 省略8行
Thread2 produce task 50
pool-1-thread-1 execute task, name : Thread2 name value : 40
// 省略47行
pool-1-thread-1 execute task, name : Thread2 name value : 40

Thread3 produce task 61
// 省略8行
Thread3 produce task 70

Thread4 produce task 81
// 省略8行
Thread4 produce task 90

Thread5 produce task 101
// 省略8行
Thread5 produce task 110

Thread6 produce task 121
// 省略8行
Thread6 produce task 130

Thread7 produce task 141
// 省略8行
Thread7 produce task 150
pool-1-thread-2 execute task, name : Thread7 name value : 140
// 省略6行
pool-1-thread-2 execute task, name : Thread7 name value : 140

Thread8 produce task 161
// 省略8行
Thread8 produce task 170

Thread9 produce task 181
// 省略8行
Thread9 produce task 190
pool-1-thread-4 execute task, name : Thread9 name value : 180
pool-1-thread-4 execute task, name : Thread9 name value : 180

Thread10 produce task 201
// 省略8行
Thread10 produce task 210
pool-1-thread-3 execute task, name : Thread10 name value : 200
// 省略39行
pool-1-thread-3 execute task, name : Thread10 name value : 200复制代码

虽然生产总数和消费总数都是100,可是明显有的消费多了,有的消费少了。合理推测一下,应该是在主线程放进任务后,子线程才生成。为了验证这个猜测,将线程池用 ThreadPoolExecutor 生成,并在用子线程生成任务以前,先赋值 context 并开启全部线程:异步

public static void main(String[] args) {
        // 固定线程池
        ThreadPoolExecutor executorService = new ThreadPoolExecutor(
                4,
                4,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>() );
        // 在main线程中赋值
        Context context = new Context();
        context.name = "Thread0 name";
        context.value = 0;
        InheritableThreadLocalContext.context.set(context);
        // 开启全部线程
        executorService.prestartAllCoreThreads();

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任务的线程对context进行赋值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        InheritableThreadLocalContext.context.set(contextMain);
                        // 提交任务
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            executorService.execute(() -> {
                                // 执行任务的子线程
                                Context contextChild = InheritableThreadLocalContext.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            });
                        }

                    }
            ).start();
        }
    }复制代码

结果不出所料,执行任务的线程输出的,都是最外面主线程设置的值。工具

那么咱们该如何才能达到最初想要的效果呢?就是利用线程池执行任务时,如何可以让执行者线程可以获取调用者线程的 context 呢?性能

使用 TransmittableThreadLocal 解决

上面的问题主要是由于执行任务的线程是被线程池管理,能够被复用(能够称为池化复用)。那复用了以后,若是仍是依赖于父线程的 context,天然是有问题的,由于咱们想要的效果是执行线程获取调用线程的 context,这时候就是TransmittableThreadLocal出场了。测试

TransmittableThreadLocal 是阿里提供的工具类,其主要解决的就是上面遇到的问题。那么该如何使用呢?spa

首先,你须要引入相应的依赖:线程

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.0</version>
</dependency>复制代码

具体代码,就拿上文提到的状况,咱们用 TransmittableThreadLocal 作一个改造:rest

public class TransmittableThreadLocalTest {
    private static TransmittableThreadLocal<Context> context = new TransmittableThreadLocal<>();

    static class Context {

        String name;

        int value;
    }

    public static void main(String[] args) {
        // 固定线程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任务的线程对context进行赋值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        TransmittableThreadLocalTest.context.set(contextMain);
                        // 提交任务
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            Runnable task = () -> {
                                // 执行任务的子线程
                                Context contextChild = TransmittableThreadLocalTest.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            };
                            // 额外的处理,生成修饰了的对象ttlRunnable
                            Runnable ttlRunnable = TtlRunnable.get(task);
                            executorService.execute(ttlRunnable);
                        }

                    }
            ).start();
        }
    }
}复制代码

此时再次运行,就会发现执行线程运行时的输出内容是彻底能够和调用线程对应上的了。固然了,我这种方式是修改了 Runnable 的写法,阿里也提供了线程池的写法,简单以下:

public static void main(String[] args) {
        // 固定线程池
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        // 额外的处理,生成修饰了的对象executorService
        executorService = TtlExecutors.getTtlExecutorService(executorService);
        ExecutorService finalExecutorService = executorService;

        for (int i = 1; i <= 10; i++) {
            int finalI = i;
            new Thread(
                    () -> {
                        // 生成任务的线程对context进行赋值
                        Context contextMain = new Context();
                        contextMain.name = String.format("Thread%s name", finalI);
                        contextMain.value = finalI * 20;
                        TransmittableThreadLocalTest.context.set(contextMain);
                        // 提交任务
                        for (int j = 1; j <= 10; j++) {
                            System.out.println("Thread" + finalI + " produce task " + (finalI * 20 + j));
                            Runnable task = () -> {
                                // 执行任务的子线程
                                Context contextChild = TransmittableThreadLocalTest.context.get();
                                System.out.println(Thread.currentThread().getName() + " execute task, name : " + contextChild.name + " value : " + contextChild.value);
                            };
                            finalExecutorService.execute(task);
                        }

                    }
            ).start();
        }
    }复制代码

其实还有更加简单的写法,具体能够参考其github:https://github.com/alibaba/transmittable-thread-local

总结

其实两篇 ThreadLocal 升级文章的出现,都是由于周三听了一个部门关于 TTL 的分享会,也是介绍了 TransmittableThreadLocal,但由于携程商旅面临国际化的改动,当前的语种信息确定是存储在线程的 context 中最方便,但涉及到线程传递的问题(由于会调用异步接口等等),因此天然就须要考虑这个了。性能方面的话,他们有作过测试,但我也只是一个听者,并无具体使用过,你们也能够一块儿交流。

有兴趣的话能够访问个人博客或者关注个人公众号、头条号,说不定会有意外的惊喜。

death00.github.io/

公众号:健程之道

相关文章
相关标签/搜索