4

我已经制作了一个组件,它实现ApplicationListener<AuthenticationSuccessEvent>并应该记录 IP 地址,IP 地址可以从HttpServletRequest. 显然这个组件在子线程中运行,因此我需要ThreadContextInheritable在组件上将属性设置为 trueDispatcherServlet才能访问HttpServletRequest. (我尝试使用 RequestContextListener 但没有效果)。

在可以在这里找到的 spring 文档中,当您设置ThreadContextInheritabletrue.

警告:如果您访问的线程池被配置为可能按需添加新线程(例如 JDK ThreadPoolExecutor),则不要对子线程使用继承,因为这会将继承的上下文暴露给这样的池线程。

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

4

1 回答 1

5

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

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

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

此代码片段将解释这一点 -

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
        

一切看起来都很好。在每个请求中,都会创建新线程,并且子线程正确地继承父线程的本地线程值。

执行 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


        

仅对于前两个请求,线程本地继承正常工作,因为线程池大小为 2,因此对于前两个请求,新线程被创建和池化。第三个请求已经从池中创建的线程被重用,它们仍然具有旧线程的继承值。

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

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

这将导致意外的信息泄漏,因为池中的工作/子线程会将一个线程的线程本地属性值泄漏到另一个线程。

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

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

于 2020-09-07T17:10:54.530 回答