线程池传递上下文参数问题

多线程传递上下文变量问题

项目中一般都会通过ThreadLocal实现上下文变量,但是多线程环境中,ThreadLocal无法在父子线程中共享数据

public class ExecutorDemo001 {
    public static final ThreadLocal<String> TL = new ThreadLocal<>();

    public void foo() {
        ThreadPoolTaskExecutor springPool = new ThreadPoolTaskExecutor();
        springPool.setCorePoolSize(1);
        springPool.setMaxPoolSize(2);
        springPool.setKeepAliveSeconds(1000);
        springPool.setQueueCapacity(10);
        springPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        springPool.initialize();

        TL.set("aaa");

        for (int i = 0; i < 100; i++) {
            springPool.submit(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                System.out.println(Thread.currentThread().getName() + "---" + TL.get());
            });
        }
    }
}

如上打印结果会出现

ThreadPoolTaskExecutor-1---null
ThreadPoolTaskExecutor-2---null
ThreadPoolTaskExecutor-1---null
main---aaa
ThreadPoolTaskExecutor-2---null
main---aaa
ThreadPoolTaskExecutor-1---null
ThreadPoolTaskExecutor-2---null
main---aaa
ThreadPoolTaskExecutor-1---null
main---aaa
ThreadPoolTaskExecutor-2---null
ThreadPoolTaskExecutor-1---null
ThreadPoolTaskExecutor-2---null
ThreadPoolTaskExecutor-1---null
ThreadPoolTaskExecutor-1---null

有三种解决方案

  1. JDK 提供的 InheritableThreadLocal
  2. 使用Spring线程池传递线程上下文
  3. Alibaba 提供的 TransmittableThreadLocal

JDK 提供的 InheritableThreadLocal

InheritableThreadLocal支持上下文传递,

原理是父线程创建子线程的时候,Thread 的 init 方法中如果查看到父线程内部有 InheritableThreadLocal 的数据。那就在子线程初始化的时,把父线程的 InheritableThreadLocal 拷贝给子线程。也就是说在线程池中无法使用

image-20250703133109828

使用Spring线程池传递线程上下文

可以通过Spring线程池的装饰器实现线程执行前ThreadLocal变量的设置

同时为了防止内存泄漏(详见ThreadLocal搭配线程池导致内存泄漏),需要在线程池中的线程执行完后清理ThreadLocal变量

@Slf4j
public class ExecutorDemo001 {
    public static final ThreadLocal<String> TL = new ThreadLocal<>();

    public void foo() {
        ThreadPoolTaskExecutor springPool = new ThreadPoolTaskExecutor();
        springPool.setCorePoolSize(1);
        springPool.setMaxPoolSize(2);
        springPool.setKeepAliveSeconds(1000);
        springPool.setQueueCapacity(10);
        springPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        springPool.setTaskDecorator(runnable -> {
            String tl = TL.get();
            return () -> {
                try {
                    TL.set(tl);
                    runnable.run();
                } catch (Exception e) {
                    log.error("decorate ctx...", e);
                } finally {
                    TL.remove();
                }
            };
        });
        springPool.initialize();

        TL.set("aaa");

        for (int i = 0; i < 100; i++) {
            springPool.submit(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log.info("{}---{}", Thread.currentThread().getName(), TL.get());
            });
        }

        log.info(String.valueOf(TL));
    }

结果

main---aaa
ThreadPoolTaskExecutor-2---aaa
ThreadPoolTaskExecutor-1---aaa
ThreadPoolTaskExecutor-1---aaa
main---null
ThreadPoolTaskExecutor-2---aaa
ThreadPoolTaskExecutor-2---aaa
ThreadPoolTaskExecutor-1---aaa
main---null
ThreadPoolTaskExecutor-1---aaa
main---null
ThreadPoolTaskExecutor-2---aaa

可以看到,后续main方法执行的时候,获取不到主线程的TL变量数据了

因为如果阻塞队列满了,拒绝策略是主线程自己执行的时候,依然会调用装饰器的方法,这个时候主线程的上下文也会被清理掉,所以清理ThreadLocal的时候需要额外判断线程id是否跟主线程一致,如果一致则不能清理

@Slf4j
public class ExecutorDemo001 {
    public static final ThreadLocal<String> TL = new ThreadLocal<>();

    public void foo() {
        ThreadPoolTaskExecutor springPool = new ThreadPoolTaskExecutor();
        springPool.setCorePoolSize(1);
        springPool.setMaxPoolSize(2);
        springPool.setKeepAliveSeconds(1000);
        springPool.setQueueCapacity(10);
        springPool.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        springPool.setTaskDecorator(runnable -> {
            String currentThreadName = getCurrentThreadName();
            String tl = TL.get();
            return () -> {
                try {
                    TL.set(tl);
                    runnable.run();
                } catch (Exception e) {
                    log.error("decorate ctx...", e);
                } finally {
                    if (!getCurrentThreadName().equals(currentThreadName)) {
                        TL.remove();
                    }
                }
            };
        });
        springPool.initialize();

        TL.set("aaa");

        for (int i = 0; i < 100; i++) {
            springPool.submit(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                log.info("{}---{}", Thread.currentThread().getName(), TL.get());
            });
        }
    }

    public static String getCurrentThreadName() {
        Thread currentThread = Thread.currentThread();
        return currentThread.getThreadGroup().getName() + "-" + currentThread.getName() + "-" + currentThread.getId();
    }
}

运行结果

ThreadPoolTaskExecutor-2---aaa
main---aaa
ThreadPoolTaskExecutor-1---aaa
main---aaa
ThreadPoolTaskExecutor-2---aaa
ThreadPoolTaskExecutor-1---aaa
ThreadPoolTaskExecutor-1---aaa
ThreadPoolTaskExecutor-2---aaa
main---aaa
ThreadPoolTaskExecutor-1---aaa
ThreadPoolTaskExecutor-2---aaa
main---aaa

Alibaba 提供的 TransmittableThreadLocal

阿里的开源项目 TransmittableThreadLocal

参考

ThreadLocal夺命11连问
InheritableThreadLocal使用详解

未完待续
未完待续

posted @ 2025-05-06 23:30  一只盐桔鸡  阅读(99)  评论(0)    收藏  举报