在我们实现业务的时候会将一些仅用于当前线程的数据使用
ThreadLocal
保存起来,在业务执行的某个时机将其取出来,因为线程隔离的特性让我们不必担心其他线程访问到错误的数据。
最近遇见一个BUG,相同的业务在请求中获取到了不同的返回结果。后来发现是`ThreadLocal``引起的问题。
ThreadLocal
的数据是线程隔离的这个很多人都介绍过了,随着线程销毁,数据也会销毁。那么不同的请求能访问到其他请求中的数据有一个原因可能是ThreadLocal
中的数据使用完后未被销毁,而线程被重用了。这个场景可以使用线程池模拟。
下面代码中模拟向ThreadLocal
中设置参数,线程池会重用固定的几个线程,一旦线程重用ThreadLocal
中数据如果没被清空则会出现新的任务读取到旧数据
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(2,2,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(12));
List<Runnable> runList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
runList.add(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
String value = threadLocal.get();
if (value == null) {
threadLocal.set(Thread.currentThread().getName());
System.out.println("threadLocal 不存在数据,设置参数 :" + Thread.currentThread().getName());
} else {
System.out.println("threadLocal 已经存在数据,参数 :" + value);
}
});
}
runList.forEach(executorService::execute);
executorService.shutdown();
}
在输出的结果中可以看到后面几条任务已经出现错误,读取到了之前线程的数据
当前线程:pool-1-thread-1
当前线程:pool-1-thread-2
threadLocal 不存在数据,设置参数 :pool-1-thread-1
threadLocal 不存在数据,设置参数 :pool-1-thread-2
当前线程:pool-1-thread-1
threadLocal 已经存在数据,参数 :pool-1-thread-1
当前线程:pool-1-thread-2
threadLocal 已经存在数据,参数 :pool-1-thread-2
当前线程:pool-1-thread-1
threadLocal 已经存在数据,参数 :pool-1-thread-1
上面的例子中可以发现,存在线程重用的场景下时ThreadLocal
的数据如果没有及时清理会导致重用此线程的业务读取错误的数据。而
Tomcat
在执行请求的工作线程就是从线程池中获取的,在官方文档中关于线程池的配置https://tomcat.apache.org/tomcat-8.5-doc/config/executor.html#Standard_Implementation
的文章中指明了默认的线程池org.apache.catalina.core.StandardThreadExecutor
。
通过分析初始化的方法和里面的逻辑可以发现,StandardThreadExecutor的实现本质上通过ThreadPoolExecutor实现业务的。
@Override
protected void startInternal() throws LifecycleException {
taskqueue = new TaskQueue(maxQueueSize);
TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf);
executor.setThreadRenewalDelay(threadRenewalDelay);
if (prestartminSpareThreads) {
executor.prestartAllCoreThreads();
}
taskqueue.setParent(executor);
setState(LifecycleState.STARTING);
}
需要注意的是:Tomcat的线程池的名字也叫作ThreadPoolExecutor,是继承了JDK的ThreadPoolExecutor然后进行了一些逻辑封装。
Tomcat的线程队列保存在org.apache.tomcat.util.threads.TaskQueue
中就是上面这段代码taskqueue = new TaskQueue(maxQueueSize)
。
可以看到Tomcat为了提供处理请求的效率也是使用线程池来处理请求的,这意味的线程会被重用,这样在使用一些线程变量的时候,如果在任务结束后没有主动请求这些数据,这些数据就会污染线程导致后续业务错误。
ThreadLocal
产生的问题却不是ThreadLocal
的问题ThreadLocal
作为一个可以设置线程共用数据的工具,能实现很多业务。很多时候我们在实现某些逻辑的时候没有意识到这些逻辑一直运行在一个多线程环境。用好ThreadLocal
,不仅要理解ThreadLocal
也要理解运行ThreadLocal
的环境。有时ThreadLocal
的问题却不是ThreadLocal
产生的问题