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

huangapple go评论83阅读模式
英文:

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 文档,当你将 ThreadContextInheritable 设置为 true 时,文档中给出了以下警告:

> 警告:如果您正在访问配置为可能根据需要添加新线程的线程池(例如 JDK 的 ThreadPoolExecutor),请不要对子线程使用继承,因为这将向这样的池化线程公开继承的上下文。

我的问题是:为什么将继承的上下文公开给池化线程会是一件糟糕的事情?在这种情况下可能出什么问题?另外,他们所指的是使用 ThreadPoolExecutor 实例的子线程,还是指使用 ThreadPoolExecutor 创建的子线程?

英文:

I have made a component that implements ApplicationListener<AuthenticationSuccessEvent> and should log the ip address, the ip address can be retrieved from HttpServletRequest. Apparently this component is run in a child thread and therefore i needed to set the property ThreadContextInheritable to true on the DispatcherServlet component in order to gain access to HttpServletRequest. (I tried using a RequestContextListener but that had no effect).

In the spring documentation which can be found here it states the following warning when you set ThreadContextInheritable to 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.

My question is: why would exposing the inherited context to a pooled thread be such a bad thing? What could go wrong in such a case? Also, do they mean a child thread that is using a ThreadPoolExecutor instance OR do they mean a child thread that is created using a ThreadPoolExecutor?

答案1

得分: 6

InheritableThreadLocal<T> 继承自 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&lt;Object&gt; value = new InheritableThreadLocal&lt;&gt;(); 

		//3. create new command and execute using thread pool created in step 1
		for (int i = 1; i &lt;= noOfThreads; i++) {
			value.set(i);
			executor.execute(() -&gt; printThreadLocalValue(value));
		}	
		executor.shutdown();
	}
   
	private static void printThreadLocalValue(ThreadLocal&lt;Object&gt; value) {
		System.out.println(Thread.currentThread().getName() + &quot; = &quot; + value.get());
	}
}
Execution 1: noOfThreads = threadPoolSize = 5
	输出:输出可能以不同的顺序出现
		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

一切看起来都很好。
因为每次请求都会创建新线程,子线程正确地继承了父线程的线程局部值。

Execution 2: noOfThreads = 5 && threadPoolSize = noOfThreads/2 = 2
  输出:
  		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,所以前两个请求会创建并池化新线程。
从第三个请求开始,池中已创建的线程将被重用,这些线程仍然具有旧线程的继承值。

Execution 2 是一个现实世界的场景,池中的线程将会被重用。

在使用线程池和 InheritableThreadLocal 组合时可能出现什么问题?

它会导致意外的信息泄漏,因为来自池的工作线程/子线程将会泄漏一个线程的线程局部属性值给另一个线程。

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

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

英文:

InheritableThreadLocal<T> extends ThreadLocal and used when we need to pass parent thread-local attribute values automatically to child thread on creation.

This inheritance works perfectly fine when you are creating new child thread each time and not reusing already created thread.

Let us consider a scenario -
We have thread pool of size 5.
When we send first 5 tasks to thread pool to process for each task new thread is created so inheritance of thread-local attribute works perfectly fine.
Problem starts from 6th request.
When you send 6th task no new thread gets created but already created thread from thread pool is reused to process it.
On 6th request parent thread-local attribute values doesn't get inherited to the child thread as we are not creating new thread but reusing already created thread from pool.

This code snippet will explain the point -

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&lt;Object&gt; value = new InheritableThreadLocal&lt;&gt;(); 

		//3. create new command and execute using thread pool created in step 1
		for (int i = 1; i &lt;= noOfThreads; i++) {
			value.set(i);
			executor.execute(() -&gt; printThreadLocalValue(value));
		}	
		executor.shutdown();
	}
   
	private static void printThreadLocalValue(ThreadLocal&lt;Object&gt; value) {
		System.out.println(Thread.currentThread().getName() + &quot; = &quot; + value.get());
	}
}
Exection 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

Everything looks good.
As on every request new thread get created and child thread correctly
inherits the thread-local values of parent.

Execution 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

Only for first two requests thread-local inheritance works correctly,
as thread pool size is 2 so for first two requests new threads
get created and pooled.
Third request onward already created threads from pool get reused
which still have the inherited values of old thread.

Execution 2 is a real world scenario where threads from pool will get reused.

What can go bad when we use Thread pool and InheritableThreadLocal combination?

It will cause unintended information leak as worker/child thread from pool will leak thread-local attribute values of one thread to another.

If your business logic is dependent on these attribute values, you will get hard to track bugs.

That's why spring documentation warns against using this combo of Thread pool and InheritableThreadLocal.

huangapple
  • 本文由 发表于 2020年8月29日 05:18:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/63640980.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定