为什么使用 ThreadPoolExecutor 的子线程不能暴露给父线程的任何继承上下文?

Why must a child thread that uses a ThreadPoolExecutor not be exposed to any inherited context of the parent thread?

我做了一个实现ApplicationListener<AuthenticationSuccessEvent>的组件,应该记录ip地址,ip地址可以从HttpServletRequest中检索。显然这个组件在子线程中是 运行,因此我需要在 DispatcherServlet 组件上将 属性 ThreadContextInheritable 设置为 true 才能访问 HttpServletRequest. (我尝试使用 RequestContextListener 但没有效果)。

在 spring 文档 which can be found here 中,当您将 ThreadContextInheritable 设置为 true 时会出现以下警告。

WARNING: Do not use inheritance for child threads if you are accessing a thread pool which is configured to potentially add new threads on demand (e.g. a JDK ThreadPoolExecutor), since this will expose the inherited context to such a pooled thread.

我的问题是:为什么将继承的上下文暴露给池线程是一件坏事?在这种情况下会出什么问题?另外,它们是指使用 ThreadPoolExecutor 实例的子线程还是指使用 ThreadPoolExecutor 创建的子线程?

InheritableThreadLocal 扩展了 ThreadLocal 并在我们需要在创建时将父 thread-local 属性值自动传递给子线程时使用。

当您每次都创建新的子线程而不重用已创建的线程时,这种继承工作得很好。

让我们考虑一个场景—— 我们有大小为 5 的线程池。 当我们将前 5 个任务发送到线程池以处理每个任务时,都会创建新线程,因此 thread-local 属性的继承工作得很好。 问题从第 6 个请求开始。 当您发送第 6 个任务时,不会创建新线程,但会重新使用线程池中已创建的线程来处理它。 在第 6 次请求时,父 thread-local 属性值不会继承到子线程,因为我们没有创建新线程,而是重用池中已经创建的线程。

此代码片段将解释要点 -

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class DeadlyComboTest {

    public static void main(String[] args) throws InterruptedException {    
        int noOfThreads     = 5;
        //Execution 1
        int threadPoolSize  = noOfThreads;
        
        //Execution 2 : uncomment below line and comment line above
        //int threadPoolSize = noOfThreads/2;
            
        //1. create thread pool
        ExecutorService executor = Executors.newFixedThreadPool(threadPoolSize);  

        //2. create Inheritable thread local
        InheritableThreadLocal<Object> value = new InheritableThreadLocal<>(); 

        //3. create new command and execute using thread pool created in step 1
        for (int i = 1; i <= noOfThreads; i++) {
            value.set(i);
            executor.execute(() -> printThreadLocalValue(value));
        }   
        executor.shutdown();
    }
   
    private static void printThreadLocalValue(ThreadLocal<Object> value) {
        System.out.println(Thread.currentThread().getName() + " = " + value.get());
    }
}
执行 1:noOfThreads = threadPoolSize = 5
    OUTPUT: you may get the output in different sequence
        pool-1-thread-1 = 1
        pool-1-thread-2 = 2
        pool-1-thread-3 = 3
        pool-1-thread-4 = 4
        pool-1-thread-5 = 5
        

一切看起来都很好。 根据每个请求创建新线程并正确创建子线程 继承父级的 thread-local 值。

执行 2:noOfThreads = 5 && threadPoolSize = noOfThreads/2 = 2
  OUTPUT:
        pool-1-thread-1 = 1
        pool-1-thread-2 = 2
        pool-1-thread-1 = 1
        pool-1-thread-2 = 2
        pool-1-thread-1 = 1


        

只有前两个请求 thread-local 继承工作正常, 由于线程池大小为 2,因此对于前两个请求新线程 得到创建和汇集。 第三个请求已经从池中创建的线程被重用 它仍然具有旧线程的继承值。

执行 2 是一个真实场景,池中的线程将被重用。

当我们使用线程池和 InheritableThreadLocal 组合时会出现什么问题?

这将导致意外的信息泄漏,因为池中的 worker/child 线程会将一个线程的 thread-local 属性值泄漏到另一个线程。

如果您的业务逻辑依赖于这些属性值,您将很难跟踪错误。

这就是为什么 spring 文档警告不要使用这种线程池和 InheritableThreadLocal 的组合。