ThreadLocal 的使用及踩坑

简介: ThreadLocal 的使用及踩坑

现在有这么一段代码,使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,来暂且代表需要在线程中保存的用户信息,这个值初始是 null。


在业务逻辑中,我先从 ThreadLocal 获取一次值,然后把外部传入的参数设置到 ThreadLocal 中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。


private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);
@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {
    //设置用户信息之前先查询一次ThreadLocal中的用户信息
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    //设置用户信息到ThreadLocal
    currentUser.set(userId);
    //设置用户信息之后再查询一次ThreadLocal中的用户信息
    String after  = Thread.currentThread().getName() + ":" + currentUser.get();
    //汇总输出两次查询结果
    Map result = new HashMap();
    result.put("before", before);
    result.put("after", after);
    return result;
}

在设置用户信息之前第一次获取的值始终应该是 null,但我们要意识到,程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的。


顾名思义,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。


为了更快地重现这个问题,我在配置文件中设置一下 Tomcat 的参数,把工作线程池最大线程数设置为 1,这样始终是同一个线程在处理请求:



server.tomcat.max-threads=1

运行程序后先让用户 1 来请求接口,可以看到第一和第二次获取到用户 ID 分别是 null 和 1,符合预期:

1.png

随后用户 2 来请求接口,这次就出现了 Bug,第一和第二次获取到用户 ID 分别是 1 和 2,显然第一次获取到了用户 1 的信息,原因就是 Tomcat 的线程池重用了线程。从图中可以看到,两次请求的线程都是同一个线程:http-nio-8080-exec-1。

2.png


这个例子告诉我们,在写业务代码时,首先要理解代码会跑在什么线程上:


  • 我们可能会抱怨学多线程没用,因为代码里没有开启使用多线程。但其实,可能只是我们没有意识到,在 Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题。


  • 因为线程的创建比较昂贵,所以 Web 服务器往往会使用线程池来处理请求,这就意味着线程会被重用。这时,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。


理解了这个知识点后,我们修正这段代码的方案是,在代码的 finally 代码块中,显式清除 ThreadLocal 中的数据。这样一来,新的请求过来即使使用了之前的线程也不会获取到错误的用户信息了。修正后的代码如下:


@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
    String before  = Thread.currentThread().getName() + ":" + currentUser.get();
    currentUser.set(userId);
    try {
        String after = Thread.currentThread().getName() + ":" + currentUser.get();
        Map result = new HashMap();
        result.put("before", before);
        result.put("after", after);
        return result;
    } finally {
        //在finally代码块中删除ThreadLocal中的数据,确保数据不串
        currentUser.remove();
    }
}

3.png


目录
相关文章
|
6月前
|
存储 安全 Java
面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
字节面试题:用过ThreadLocal吗?ThreadLocal是在哪个包下的?看过ThreadLocal源码吗?讲一下ThreadLocal的get和put是怎么实现的?
77 0
|
5月前
|
存储 安全 Java
《ThreadLocal使用与学习总结:》史上最详细由浅入深解析ThreadLocal
《ThreadLocal使用与学习总结:》史上最详细由浅入深解析ThreadLocal
43 0
|
5月前
|
存储 Java 数据库连接
来探讨一下最近面试问的ThreadLocal问题
来探讨一下最近面试问的ThreadLocal问题
|
存储 缓存 安全
面试~ThreadLocal
面试~ThreadLocal
67 0
|
存储 Java
大厂是怎么用ThreadLocal?ThreadLocal核心原理分析
ThreadLocal**是Java中的一个线程本地变量类。它可以让每个线程都有自己独立的变量副本,而不会相互影响。
117 1
|
Java 数据库连接
ThreadLocal原理和实践
ThreadLocal是线程本地变量,解决多线程环境下成员变量共享存在的问题。ThreadLocal为每个线程创建独立的的变量副本,他的特性是该变量的引用对全局可见,但是其值只对当前线程可用,每个线程都将自己的值保存到这个变量中而各线程不受影响。
160 0
ThreadLocal原理和实践
|
Java
ThreadLocal笔记
ThreadLocal笔记
201 0
ThreadLocal笔记
|
存储 安全 Java
|
安全 Java
ThreadLocal相关知识点
ThreadLocal相关知识点
109 0
ThreadLocal相关知识点
|
算法 安全 Java
threadlocal再温习
早时总结过《ThreadLocal解析》、《FastThreadLocal解析》 最近看一些资料的时候,又重重发现了这类,不希望再温下,许多知识点,之前已经总结了,这篇文章主要有两个问题: 1、弱引用的意义 2、如何防键冲突
239 0
threadlocal再温习