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


目录
相关文章
|
NoSQL Linux 测试技术
Linux下Redis的安装、配置及开机自启动
Linux下Redis的安装、配置及开机自启动 系统版本: CentOS 7 Redis版本: Redis-6.2.5
47973 7
Linux下Redis的安装、配置及开机自启动
|
NoSQL Shell MongoDB
解决在cmd中输入mongo出现‘mongo‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件的问题~
解决在cmd中输入mongo出现‘mongo‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件的问题~
1666 0
|
缓存 NoSQL 数据库
探秘Redis读写策略:CacheAside、读写穿透、异步写入
本文介绍了 Redis 的三种高可用性读写模式:CacheAside、Read/Write Through 和 Write Behind Caching。CacheAside 简单易用,但可能引发数据不一致;Read/Write Through 保证数据一致性,但性能可能受限于数据库;Write Behind Caching 提高写入性能,但有数据丢失风险。开发者应根据业务需求选择合适模式。
2328 2
探秘Redis读写策略:CacheAside、读写穿透、异步写入
|
SQL 存储 运维
Flyway基本介绍及基本使用
使用5W1H方式介绍一下在Java项目开发中使用Flyway来管理数据库版本。。
10652 0
Flyway基本介绍及基本使用
|
缓存 Java 应用服务中间件
一文带你使用xxl-job定时任务
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。 将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。 因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;
5118 0
一文带你使用xxl-job定时任务
|
XML NoSQL Java
Redis - 一篇走心的 RedisUtil 工具类
Redis - 一篇走心的 RedisUtil 工具类
4177 0
Redis - 一篇走心的 RedisUtil 工具类
|
10月前
|
存储 安全 Java
ThreadLocal - 原理与应用场景详解
ThreadLocal是Java中用于实现线程隔离的重要工具,为每个线程提供独立的变量副本,避免多线程数据共享带来的安全问题。其核心原理是通过 ThreadLocalMap 实现键值对存储,每个线程维护自己的存储空间。ThreadLocal 广泛应用于线程隔离、跨层数据传递、复杂调用链路的全局参数传递及数据库连接管理等场景。此外,InheritableThreadLocal 支持子线程继承父线程的变量值,而 TransmittableThreadLocal 则解决了线程池中变量传递的问题,提升了多线程上下文管理的可靠性。深入理解这些机制,有助于开发者更好地解决多线程环境下的数据隔离与共享挑战。
1897 43
|
缓存 NoSQL Java
RedisTemplate操作Redis,这一篇文章就够了
redis是一款开源的Key-Value数据库,运行在内存中,由C语言编写。企业开发通常采用Redis来实现缓存。同类的产品还有memcache 、memcached 等。
3381 1
|
Java 应用服务中间件
Process exited with an error: 1 (Exit value: 1) 问题处理
本文介绍了在本地启动项目时遇到“Process exited with an error: 1”错误的解决过程。作者首先排除了配置文件修改的影响,通过分析错误信息,推测为端口占用问题,并使用 `netstat -ano` 命令检查端口占用情况,最终通过终止占用端口的进程解决了问题。
|
Java 开发者 Spring
springboot @Primary的概念与使用
【4月更文挑战第26天】在 Spring Framework 中,@Primary 注解用于标记一个 bean 作为在多个同类型的 bean 候选中进行自动装配时的首选 bean。这个注解非常有用,在配置和自动装配复杂的 Spring 应用程序时尤其如此,特别是当有多个 bean 实现相同的接口或继承相同的类时
1417 3

热门文章

最新文章