解决:InheritableThreadLocal与线程池共用的问题

文章探讨了InheritableThreadLocal如何在与线程池交互时无法继承父线程的变量,提供了三种解决方案:避免共用、参数传递和使用SpringThreadPoolTaskExecutor的装饰器进行任务上下文管理。
摘要由CSDN通过智能技术生成

回顾一下上篇文章:InheritableThreadLocal和ThreadLocal的区别和使用场景
上篇文章介绍道,InheritableThreadLocal 是 ThreadLocal 的一个子类,它不但继承了ThreadLocal的所有特性,父线程中的 InheritableThreadLocal 变量的值可以被子线程继承。

为什么InheritableThreadLocal不能被线程池继承

上篇文章的最后留下一个引申思考,当InheritableThreadLocal 与线程池共用时,父线程中的 InheritableThreadLocal 变量的值能否被线程池中的工作线程继承?
答案是否定的,InheritableThreadLocal能够实现继承的源代码是存在于Thread类的构造器中,也就是说在父线程中new出来子线程才会实现InheritableThreadLocal继承。我们都知道线程池的特点是线程复用,线程池中的核心工作线程一旦创建,将长时间存在于线程池中。所以父线程使用线程池来分解任务时,并不会新建子线程,而是复用已有的线程,就不会触发Thread类的构造器的代码,导致父线程中的 InheritableThreadLocal 变量的值不能被线程池中的工作线程继承。

代码复现InheritableThreadLocal 与线程池共用

这里案例写的有点冗长,我想清楚还原线程池的实际运营状况。
大体思路:

  1. 先用高耗时的多个任务将线程池的核心线程数打满,确认刚开始线程池是能够继承InheritableThreadLocal的。
  2. 核心线程满了之后,修改父线程的InheritableThreadLocal变量
  3. 再提交新任务给线程池,确认线程池开始复用线程后就不能继承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 Main
        threadLocal.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实例(详解优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

怪力乌龟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值