线程的专属储物柜:ThreadLocal想象一下,你有一个秘密基地,一个只有你自己知道的地方,你可以在那里存放你的私人物
引用
想象一下,你有一个秘密基地,一个只有你自己知道的地方,你可以在那里存放你的私人物品,比如日记、收藏品或是零食。这个地方完全属于你,别人没有钥匙,无法进入。这就是ThreadLocal在多线程编程中的工作原理,每个线程都有它自己的“秘密空间”。
一、认识ThreadLocal
ThreadLocal 是 Java 中的一个类,它提供了线程局部变量。每个使用该变量的线程都有独立的变量副本,因此每个线程都可以在不影响其他线程的情况下更改自己的副本。
1.1 ThreadLocal的定义
ThreadLocal 类的实例通常用于存储每个线程的私有数据。它提供了一种线程安全的机制,用于存储线程特定的数据,而不需要同步。
1.2 ThreadLocal 与全局变量的区别
特性 | ThreadLocal | 全局变量 |
---|---|---|
作用域 | 作用域限定在线程级别,每个线程有独立副本 | 作用域是全局的,所有线程共享同一个变量 |
数据隔离 | 每个线程只能访问自己的数据 | 所有线程都可以访问和修改数据 |
线程安全 | 每个线程有自己的数据副本,天然线程安全 | 需要额外同步机制来保证线程安全 |
内存使用 | 每个线程有自己的数据副本,可能会使用更多内存 | 共享变量内存使用相对较少 |
生命周期 | 通常与线程的生命周期相关联 | 通常与应用程序生命周期一致 |
使用场景 | 适用于需要线程隔离数据的场景 | 适用于需要在所有线程间共享数据的场景 |
二、ThreadLocal的工作原理
2.1 线程与ThreadLocalMap的关系
在Java中,ThreadLocal是一种线程局部变量工具,它提供了线程隔离的存储方式,使得每个线程可以拥有自己的变量副本,这样各个线程之间的数据就不会相互干扰了。ThreadLocal在内部使用ThreadLocalMap来存储每个线程的局部变量。每个线程的Thread对象都有一个ThreadLocalMap的实例,这个映射是线程私有的,它包含了线程局部变量的副本。
2.2 ThreadLocalMap的内部结构
ThreadLocalMap是ThreadLocal的内部类,它是一个定制的哈希表,专门用于存储线程局部变量。它的键是ThreadLocal对象,值是线程局部变量的值。ThreadLocalMap的结构如下:
-
INITIAL_CAPACITY: 初始容量,必须是2的幂次方。
-
table: 存放数据的数组,数组长度也必须是2的幂次方。
-
size: 数组中entry的数量。
-
threshold: 扩容的阈值,当size超过这个值时,会进行扩容操作。
-
存储结构:ThreadLocalMap的存储结构是Entry,它继承自WeakReference,使用弱引用来引用ThreadLocal对象。这样可以在没有其他强引用时,允许ThreadLocal对象被垃圾回收器回收,从而避免内存泄漏。
2.3 ThreadLocal的初始值设定
ThreadLocal允许子类重写initialValue()方法来设定初始值。这个方法是一个延迟调用的方法,它在get()方法被调用但还没有对应的值时执行,并且只执行一次。默认情况下,initialValue()方法返回null,但可以通过子类覆盖这个方法来提供一个非null的初始值。
public class CustomThreadLocal extends ThreadLocal<String> {
/**
* 自定义初始值
*
* @return 初始值(注意这里的返回类型由上面的泛型决定)
*/
@Override
protected String initialValue() {
return "PENDING";
}
}
三、ThreadLocal的使用场景
ThreadLocal的使用场景的使用场景可以从以下几个方面考虑:
-
数据库连接:每个线程可以拥有自己的数据库连接,这样可以避免数据库连接的共享问题。
-
事务管理:在事务处理系统中,每个线程可以有自己的事务上下文,使用 ThreadLocal 可以确保事务信息不会在线程间共享。
-
用户会话信息:在 Web 应用中,每个用户会话可以存储在 ThreadLocal 中,确保线程安全。
-
日志记录:每个线程可以有自己的日志记录器实例,这样可以记录线程特定的信息。
-
资源清理:在处理需要清理资源的场景(如文件句柄、网络连接等)时,可以使用 ThreadLocal 存储资源引用,确保在线程结束时能够正确清理。
四、ThreadLocal的注意事项
-
内存泄漏:由于 ThreadLocal 使用弱引用存储键,如果线程长期运行,可能会导致内存泄漏。因此,需要在适当的时候调用 remove() 方法清理数据。
-
线程池:在线程池中使用 ThreadLocal 要小心,因为线程会被复用,可能会导致数据污染。通常需要在任务执行完毕后手动清理 ThreadLocal。
-
继承性:如果需要子线程继承父线程的 ThreadLocal 值,可以使用 InheritableThreadLocal。
五、踩坑记录:数据源注解失效了
一个项目引入了两个不同的数据源,默认使用的是a数据源,在进行一个业务查询的时候,注解了使用b数据源,这个数据源在前面查询是OK的,但是到下面就又变回了a数据源;描述完成,但是简单看这里,可能一脸懵逼,下面跟我一起看看仔细走走。
- 下面是一个伪代码来演示,最终的结果就是第三步执行失败,因为数据源切回了
public class TestService {
@CommonDataSourceType("bDataSource")
public String updateUserBindCardStatusService(String id) {
// 步骤一、查询用户信息
User user = bUserMapper.selectByPrimaryKey(id);
if (Objects.isNull(user)) {
return "用户不存在";
}
// 步骤二、查询用户绑卡信息
List<UserBindCard> ucList = bUserService.selectUserBindCardByUserId(id);
// 步骤三、更新用户绑卡状态
bUserMapper.updateUserBindCardStatus(id, Objects.isNull(ucList));
}
}
- 针对上面的伪代码,我增加了一个流程图说明
-
问题原因:在使用
updateUserBindCardStatusService
的时候切换了数据源,在使用bUserService.selectUserBindCardByUserId
的时候又切换了一次数据源,因为数据源是在ThreadLocal
里面存储的,我们的数据源切换使用完之后就会清空,导致的问题就是,bUserService.selectUserBindCardByUserId
执行完就清空了数据源,所以出错了。 -
解决办法:找到原因就好解决了,
-
1、不要用
bUserService.selectUserBindCardByUserId
方法、改成直接查询Mapper
或者内部私有方法 -
2、修改数据源切换源码,让其能够回到上一次的状态,使用队列先进后出
-
六、总结
一句话总结:ThreadLocal为每个线程提供了一个独立的变量副本,以确保线程安全并提高性能。
希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。
同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。
感谢您的支持和理解!
转载自:https://juejin.cn/post/7419379873029226536