工作积累——Web请求中使用ThreadLocal遇见的问题

发现错误

在我们实现业务的时候会将一些仅用于当前线程的数据使用ThreadLocal 保存起来,在业务执行的某个时机将其取出来,因为线程隔离的特性让我们不必担心其他线程访问到错误的数据。

最近遇见一个BUG,相同的业务在请求中获取到了不同的返回结果。后来发现是`ThreadLocal``引起的问题。

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

Tomcat的工作线程

上面的例子中可以发现,存在线程重用的场景下时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产生的问题

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在Spring Boot,可以使用ThreadLocal来实现线程间的数据传递和共享。ThreadLocal是一个线程内部的数据存储类,每个线程都有一个独立的ThreadLocal实例,可以在其存储和获取数据,不同的线程之间互不干扰。在Spring Boot,可以将ThreadLocal作为一个工具类,用来存储一些需要在同一线程共享的数据,比如用户身份信息、请求上下文等。 在使用ThreadLocal时,通常需要定义一个静态的ThreadLocal对象,通过set()方法设置值,在需要使用的地方通过get()方法获取值。需要注意的是,在使用ThreadLocal时,需要注意内存泄漏的问题,即在不需要使用ThreadLocal时,需要及时清除ThreadLocal的数据,以免造成内存泄漏。 以下是一个示例代码,演示了如何在Spring Boot使用ThreadLocal: ``` public class UserContextHolder { private static ThreadLocal<UserContext> userContext = new ThreadLocal<>(); public static UserContext getUserContext() { return userContext.get(); } public static void setUserContext(UserContext context) { userContext.set(context); } public static void clear() { userContext.remove(); } } ``` 在上面的示例代码,定义了一个静态的ThreadLocal对象userContext,用来存储UserContext对象。通过静态的getUserContext()和setUserContext()方法,可以在任何地方获取和设置UserContext对象。在需要清除ThreadLocal的数据时,可以调用clear()方法清除ThreadLocal的数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大·风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值