likes
comments
collection
share

线程的专属储物柜:ThreadLocal想象一下,你有一个秘密基地,一个只有你自己知道的地方,你可以在那里存放你的私人物

作者站长头像
站长
· 阅读数 59

引用

想象一下,你有一个秘密基地,一个只有你自己知道的地方,你可以在那里存放你的私人物品,比如日记、收藏品或是零食。这个地方完全属于你,别人没有钥匙,无法进入。这就是ThreadLocal在多线程编程中的工作原理,每个线程都有它自己的“秘密空间”。

一、认识ThreadLocal

ThreadLocal 是 Java 中的一个类,它提供了线程局部变量。每个使用该变量的线程都有独立的变量副本,因此每个线程都可以在不影响其他线程的情况下更改自己的副本。

线程的专属储物柜:ThreadLocal想象一下,你有一个秘密基地,一个只有你自己知道的地方,你可以在那里存放你的私人物

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对象被垃圾回收器回收,从而避免内存泄漏。

线程的专属储物柜: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想象一下,你有一个秘密基地,一个只有你自己知道的地方,你可以在那里存放你的私人物

  • 数据库连接:每个线程可以拥有自己的数据库连接,这样可以避免数据库连接的共享问题。

  • 事务管理:在事务处理系统中,每个线程可以有自己的事务上下文,使用 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));
    }

}
  • 针对上面的伪代码,我增加了一个流程图说明

线程的专属储物柜:ThreadLocal想象一下,你有一个秘密基地,一个只有你自己知道的地方,你可以在那里存放你的私人物

  • 问题原因:在使用updateUserBindCardStatusService的时候切换了数据源,在使用bUserService.selectUserBindCardByUserId的时候又切换了一次数据源,因为数据源是在ThreadLocal里面存储的,我们的数据源切换使用完之后就会清空,导致的问题就是,bUserService.selectUserBindCardByUserId执行完就清空了数据源,所以出错了。

  • 解决办法:找到原因就好解决了,

    • 1、不要用bUserService.selectUserBindCardByUserId方法、改成直接查询Mapper或者内部私有方法

    • 2、修改数据源切换源码,让其能够回到上一次的状态,使用队列先进后出

六、总结

一句话总结:ThreadLocal为每个线程提供了一个独立的变量副本,以确保线程安全并提高性能。

希望本文对您有所帮助。如果有任何错误或建议,请随时指正和提出。

同时,如果您觉得这篇文章有价值,请考虑点赞和收藏。这将激励我进一步改进和创作更多有用的内容。

感谢您的支持和理解!

转载自:https://juejin.cn/post/7419379873029226536
评论
请登录