回顾一下上篇文章:InheritableThreadLocal和ThreadLocal的区别和使用场景
上篇文章介绍道,InheritableThreadLocal 是 ThreadLocal 的一个子类,它不但继承了ThreadLocal的所有特性,父线程中的 InheritableThreadLocal 变量的值可以被子线程继承。
为什么InheritableThreadLocal不能被线程池继承
上篇文章的最后留下一个引申思考,当InheritableThreadLocal 与线程池共用时,父线程中的 InheritableThreadLocal 变量的值能否被线程池中的工作线程继承?
答案是否定的,InheritableThreadLocal能够实现继承的源代码是存在于Thread类的构造器中,也就是说在父线程中new出来子线程才会实现InheritableThreadLocal继承。我们都知道线程池的特点是线程复用,线程池中的核心工作线程一旦创建,将长时间存在于线程池中。所以父线程使用线程池来分解任务时,并不会新建子线程,而是复用已有的线程,就不会触发Thread类的构造器的代码,导致父线程中的 InheritableThreadLocal 变量的值不能被线程池中的工作线程继承。
代码复现InheritableThreadLocal 与线程池共用
这里案例写的有点冗长,我想清楚还原线程池的实际运营状况。
大体思路:
- 先用高耗时的多个任务将线程池的核心线程数打满,确认刚开始线程池是能够继承InheritableThreadLocal的。
- 核心线程满了之后,修改父线程的InheritableThreadLocal变量
- 再提交新任务给线程池,确认线程池开始复用线程后就不能继承InheritableThreadLocal
public class ThreadLocalExample {// 创建一个 InheritableThreadLocal 变量private static final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();// 创建一个4个工作线程的线程池private static ThreadPoolExecutor threadPoolExecutor =new ThreadPoolExecutor(4, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));private static final AtomicInteger threadCount = new AtomicInteger();public static void main(String[] args) {// 设置主线程的 ThreadLocal 变量threadLocal.set("Thread main");// 标记一下工作线程的名字threadPoolExecutor.setThreadFactory(r -> {Thread thread = new Thread(r);thread.setName("WorkerThread-" + threadCount.incrementAndGet());return thread;});// 先写一个耗时任务将线程池打满for (int i = 0; i < 4; i++) {Thread thread = new Thread(() -> {try {Thread.sleep(1000);// 打印当前线程的 ThreadLocal 变量System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());} catch (InterruptedException e) {e.printStackTrace();}});// 将任务提交到线程池threadPoolExecutor.execute(thread);}// 等待线程池中的任务执行完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}// 确认一下线程池中的线程数System.out.println("线程池中的线程数:" + threadPoolExecutor.getPoolSize());// 再将主线程的 ThreadLocal 变量设置为 Thread Not MainthreadLocal.set("Thread not main");System.out.println("线程池中的线程已满,父线程的threadLocal值已修改为"+threadLocal.get()+",再提交一个任务到线程池中,查看子线程的threadLocal值是否会发生变化");// 再提交一个任务Thread thread = new Thread(() -> {try {Thread.sleep(1000);// 打印当前线程的 ThreadLocal 变量System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());} catch (InterruptedException e) {e.printStackTrace();}});// 将任务提交到线程池threadPoolExecutor.execute(thread);// 关闭线程池threadPoolExecutor.shutdown();}
}
执行结果:
WorkerThread-2: Thread main
WorkerThread-1: Thread main
WorkerThread-4: Thread main
WorkerThread-3: Thread main
线程池中的线程数:4
线程池中的线程已满,父线程的threadLocal值已修改为Thread not main,再提交一个任务到线程池中,查看子线程的threadLocal值是否会发生变化
WorkerThread-2: Thread main
解决方法
方法一:
避免InheritableThreadLocal与线程池共用
不要吐槽这个解决方式,实际上这是一个最安全有效的方案
方法二:
不要在线程池的工作线程中获取InheritableThreadLocal参数值,而是采用普通参数传递方式传值
让我们将上文的代码做点改造,来证明使用普通参数传递方式传值是可信的。创建一个函数printThreadLocal分别打印线程名、ThreadLocal变量值和用过传参的方式传递的ThreadLocal变量值,利用函数式编程将该方法作为子任务交给线程池处理,如下:
public class ThreadLocalExample{// 创建一个 InheritableThreadLocal 变量private static final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();// 创建一个4个工作线程的线程池private static ThreadPoolExecutor threadPoolExecutor =new ThreadPoolExecutor(4, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));private static final AtomicInteger threadCount = new AtomicInteger();private static void printThreadLocal(String str) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 打印当前线程的 ThreadLocal 变量System.out.println("当前线程:"+Thread.currentThread().getName()+";ThreadLocal变量" + threadLocal.get() + ";传递变量:" + str);}public static void main(String[] args) {// 标记一下工作线程的名字threadPoolExecutor.setThreadFactory(r -> {Thread thread = new Thread(r);thread.setName("WorkerThread-" + threadCount.incrementAndGet());return thread;});// 先写一个耗时任务将线程池打满for (int i = 0; i < 8; i++) {// 设置主线程的 ThreadLocal 变量threadLocal.set("Thread main"+i);// 在主线程中获取ThreadLocal变量的值String str = threadLocal.get();// 将任务提交到线程池,并以传参的方式传递ThreadLocal变量threadPoolExecutor.execute(()->printThreadLocal(str));}// 关闭线程池threadPoolExecutor.shutdown();}
}
执行结果
当前线程:WorkerThread-4;ThreadLocal变量Thread main3;传递变量:Thread main3
当前线程:WorkerThread-1;ThreadLocal变量Thread main0;传递变量:Thread main0
当前线程:WorkerThread-2;ThreadLocal变量Thread main1;传递变量:Thread main1
当前线程:WorkerThread-3;ThreadLocal变量Thread main2;传递变量:Thread main2
当前线程:WorkerThread-4;ThreadLocal变量Thread main3;传递变量:Thread main4
当前线程:WorkerThread-2;ThreadLocal变量Thread main1;传递变量:Thread main6
当前线程:WorkerThread-3;ThreadLocal变量Thread main2;传递变量:Thread main7
当前线程:WorkerThread-1;ThreadLocal变量Thread main0;传递变量:Thread main5
通过执行结果得知,当工作线程开始复用的时候,ThreadLocal变量就无法继承,但是通过参数传递的ThreadLocal变量一直是正确的。
方式三
本文的重点来了,各位都是优雅的程序设计师,怎么能够用以上的低级方式规避问题呢。我们必须优雅的解决InheritableThreadLocal与线程池共用的问题。
这里我给大家介绍一个线程池增强类ThreadPoolTaskExecutor ,这个类是Spring对ThreadPoolExecutor的加强,具体用法请参考我之前的文章这样用线程池才优雅-企业级线程池示例
在那个文章里,我在最后注释掉了一行配置:线程池的装饰器
//executor.setTaskDecorator(new ContextCopyingDecorator());
其实这个装饰器就可以解决InheritableThreadLocal与线程池共用的问题,翻开这个方法的doc,上面说到这个装饰器主要用于给线程任务设置一些上下文和监控,实际上相当于给工作任务做了一个切面。
是的,我们可以利用线程池装饰器(executor.setTaskDecorator())给工作线程做切面,这样就可以在任务执行之前将父线程的ThreadLocal赋值给工作线程,并能够在工作线程执行完毕后清除ThreadLocal,相当优雅。
下文将分享TaskDecorator实例(还没写)