ThreadLocal数据共享的问题

1,032 阅读3分钟

背景

threadLocal默认做了线程数据隔离,本身就是避免数据竞争。 但是有时候会遇到数据共享问题,这里就需要用不同的实现类。

线程数据共享

工具类

简介

  1. ThreadLocal

父子线程间数据是隔离的,父线程不会传递threadLocal副本到子线程

  1. InheritatbleThreadLocal

子线程创建时,父线程threadLocal副本会传递给子线程(复用线程不会传递)。当threadLocal被主线程刷新时,但是线程池线程不会重新创建,只会复用,所以新threadLocal副本无法传递。

  1. TransmittableThreadLocal

阿里工具类,继承并加强于InheritatbleThreadLocal,解决了InheritableThreadLocal中线程复用传递副本的问题。

这里就有个大坑, 必须用阿里配套的线程池工具类包装下!!! 不然跟InheritatbleThreadLocal一个效果。

  1. ThreadLocal



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



ExecutorService executorService = Executors.newFixedThreadPool(10);



PostMapping("/threadLocalAndExecutorService")

@ResponseBody

public Boolean threadLocalAndExecutorService(@RequestBody String tenantId)  {

    THREAD_LOCAL.set(tenantId);

    for (int i = 0; i < 10; i++) {

        executorService.submit( ()->{

            String content = THREAD_LOCAL.get();

            // 直接获取不到

            log.info(">>> {}  中并行流线程名为 {},内容为 {}", tenantId,Thread.currentThread().getName(), content);

        });

    }

    return true;

}

输出结果为

2021-12-09 09:38:35.856  INFO 26719 --- [pool-6-thread-4] c.y.u.controller.ThreadLocalController   : >>> 7  中并行流线程名为 pool-6-thread-4,内容为 null

2021-12-09 09:38:35.856  INFO 26719 --- [pool-6-thread-3] c.y.u.controller.ThreadLocalController   : >>> 7  中并行流线程名为 pool-6-thread-3,内容为 null

由于使用ThreadLocal, 父线程不会把threadLocal副本传递给子线程,所以子线程拿到的值都是null。

这里的子线程可以是

  1. 线程池中的复用线程
  2. 并行流中的新线程(并行流中第一个线程还是主线程,所以拿得到值)

  1. InheritableThreadLocal

注意:如果你已经跑过上面的代码,请重启服务(重置线程池)

InheritableThreadLocal适用于子线程创建时将父线程中的threadLocal副本传递给子线程。

但是生产环境中用的线程池复用,这会导致新的threadLocal副本无法放入到子线程中。

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



ExecutorService executorService = Executors.newFixedThreadPool(10);



@PostMapping("/inheritableAndExecutor")

@ResponseBody

public Boolean inheritableAndExecutor(@RequestBody String tenantId)  {

    log.info("参数传入 {}",tenantId);

    INHERITABLE_CONTEXT.set(tenantId);

    for (int i = 0; i < 10; i++) {

        executorService.submit( ()->{

            String content = INHERITABLE_CONTEXT.get();

            //会出现串环境的问题

            log.info(">>>  传入 {}  并行流线程名为 {},内容为 {}  \n",tenantId,Thread.currentThread().getName(), content);

        });

    }

    return true;

}

输出

pool-6-thread-1第一次能正常获取到值。

2021-12-09 09:41:04.851  INFO 27540 --- [pool-6-thread-1] c.y.u.controller.ThreadLocalController   : >>>  传入 10  并行流线程名为 pool-6-thread-1,内容为 10  

但是当第二次请求时,pool-6-thread-1线程没有获得到新的threadLocal副本值,这是InheritableThreadLocal的局限性。

2021-12-09 09:42:48.827  INFO 27540 --- [pool-6-thread-1] c.y.u.controller.ThreadLocalController   : >>>  传入 4  并行流线程名为 pool-6-thread-1,内容为 10  

  1. TransmittableThreadLocal

阿里提供的一个工具包,在线程池等池化复用的执行组件情况下,提供ThreadLocal值的传递功能。解决异步上传时,上下文传递问题。同时解决了threadLocal刷新,线程池无法刷新的问题。

  1. 使用TransmittableThreadLocal实现类
  2. 使用TtlExecutors.getTtlExecutorService()获得线程池


ExecutorService ttlExecutorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(10));



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



@PostMapping("/ttlAndTtlExecutorService")

@ResponseBody

public Boolean ttlAndTtlExecutorService(@RequestBody String tenantId)  {

    TRANSMITTABLE_THREAD_LOCAL.set(tenantId);

    for (int i = 0; i < 10; i++) {

        ttlExecutorService.submit( ()->{

            String content = TRANSMITTABLE_THREAD_LOCAL.get();

            //这里没有任何问题

            log.info(">>> {} 中 ttl中并行流线程名为 {},内容为 {}", tenantId,Thread.currentThread().getName(), content);

        });

    }

    return true;

}

注意:

如果不用包装类线程池,比如

  1. 直接创建线程池
  2. 并行流(parallelStream)

那么会有问题,跟InheritableThreadLocal一样,线程复用时,数据无法刷新。

参考

juejin.cn/post/699855…

github.com/alibaba/tra…