当前位置: 首页 > news >正文

深入理解ThreadLocal:线程安全的“独享空间”

目录

    • 什么是 ThreadLocal?
      • 1.应用场景
      • 2.如何使用
    • 底层原理
      • 1.数据存储
        • 1.1Thread 、ThreadLocal、ThreadLocalMap、Entry 之间的关系
      • 2.set()
      • 3.get()
      • 4.remove()
    • 存在问题
      • 1.内存堆积
        • 1.1复现过程
        • 1.2存在问题
        • 1.3解决方法
      • 2.线程池复用导致数据污染
    • 为什么 ThreadLocal 是弱引用?
      • 1.如果 key 是强引用
      • 2.采用弱引用,解决内存泄漏
      • 3.为什么 value 不是弱引用?
      • 4.正确的使用方式
    • ThreadLocal 与 Synchronized 的区别
    • 应用场景
      • 1.用户身份信息
    • Reference

什么是 ThreadLocal?

ThreadLocal 是 Java 提供的一种用于线程本地存储的工具类,它可以为每个线程提供独立的变量副本,从而实现线程隔离。ThreadLocal 主要用于在多线程环境下存储线程独有的数据,避免多个线程间共享变量带来的数据一致性问题。

1.应用场景

应用场景说明
数据库连接管理绑定 Connection到线程,避免同步问题
用户身份管理线程隔离 Session,存储用户信息
日志追踪存储 Trace ID,方便日志分析
日期格式化避免 SimpleDateFormat线程不安全问题
线程计数器统计当前线程的执行次数
线程池数据传递使用 TransmittableThreadLocal解决线程池数据丢失问题

2.如何使用

  1. 创建 ThreadLocal

创建了一个 ThreadLocal 变量 localVariable,任何一个线程都能并发访问 localVariable。

public static ThreadLocal<String> localVariable = new ThreadLocal<>();
  1. 新增数据,线程可以在任何地方写入数据
localVariable.set("测试数据");
  1. 读取数据,线程在任何地方都能够读取数据
localVariable.get();
  1. 删除数据
localVariable.remove();

底层原理

1.数据存储

ThreadLocal 变量的值存储在 Thread 内部的 ThreadLocalMap ,不是 ThreadLocal 本身。

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {  // 弱引用类型
            Object value;   // 强引用
            Entry(ThreadLocal<?> k, Object v) {
                super(k);   // ThreadLocal 类型作为key,弱引用
                value = v; 
            }
        }

        private Entry[] table;
    }
}
  1. ThreadLocalMap 采用 线性探测法 解决哈希冲突
  2. 弱引用Entry 继承 WeakReferenceThreadLocal 对象被回收时,键会变为 null,避免内存泄漏
  3. 每个 Thread 维护一个 ThreadLocalMap 实例,键是 ThreadLocal,值是变量
1.1Thread 、ThreadLocal、ThreadLocalMap、Entry 之间的关系
[Thread]
   |
   v
[ThreadLocalMap] (threadLocals)
   |
   v
[Entry[]] (table)
   |
   +--> [Entry1] --> key: WeakReference<ThreadLocalA>
   |                 value: ValueA
   |
   +--> [Entry2] --> key: WeakReference<ThreadLocalB>
                     value: ValueB

Thread 线程可以拥有多个 ThreadLocal 维护的自己线程独享的共享变量(这个共享变量只是针对自己线程里面共享)

2.set()

public void set(T value) {
    Thread t = Thread.currentThread();  // 获取当前线程
    ThreadLocalMap map = getMap(t);   // 获取线程的ThreadLocalMap
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}
  1. 先获取当前线程 ThreadLocalMap
  2. 如果 map 存在,直接存入 ThreadLocalMap
  3. 否则创建一个新的 ThreadLocalMap

3.get()

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
  1. 通过 Thread.currentThread() 获取当前线程
  2. ThreadLocalMap 获取当前 ThreadLocal 变量
  3. 若不存在,则调用 setInitialValue() 赋初值
private T setInitialValue() {
    T value = initialValue();   // 返回null值
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }

    // 检查当前对象 (this) 是否是 TerminatingThreadLocal 的实例
    // TerminatingThreadLocal 是 ThreadLocal 的子类
    ()if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

4.remove()

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        m.remove(this);
    }
}

存在问题

1.内存堆积

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {  // 弱引用
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);   // ThreadLocal 类型作为key。
                value = v;
            }
        }

        private Entry[] table;
    }
}

由于 ThreadLocalMap.Entry 采用 弱引用 存储 ThreadLocal,但值 value强引用,可能导致:

  1. ThreadLocal 被回收后,Entry.key 变为 null
  2. value 仍然存在,无法被回收,造成 内存泄漏

GC 时,

强引用:永不回收(除非手动置 null

软引用:只有在内存不足时才会清理软引用对象

弱引用:发现弱引用后,会立即回收,不会管内存是否充足。

虚引用:对象被回收后通知

表面上 keyvalue 使用的是同一个 ThreadLocal, 实际上使用的 value 却是自己独有的一份。

1.1复现过程
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set(new Object());  // 存入一个对象

threadLocal = null;  // ThreadLocal 被置为 null,意味着没有强引用指向它

执行后的引用关系

  1. ThreadLocal不再有外部强引用,GC 会回收它。
  2. ThreadLocalMap 中仍然有 Entry,只是 Entry.key 变成了 null(因为 ThreadLocal<?> 是弱引用,已经被 GC 回收)。
  3. Entry.value 仍然是一个强引用,它存储的对象不会被 GC 回收,导致内存泄漏
ThreadLocalMap (ThreadLocal 被 GC 回收后)
----------------------------------------
| Entry (null)  ->  value (强引用对象) |
----------------------------------------
1.2存在问题
  1. Entry 的 key 是弱引用,当 ThreadLocal 实例被回收后,key 变为 null
  2. 对应的 value 仍然被强引用链Thread -> ThreadLocalMap -> Entry -> value 保持;
  3. 线程池场景下线程长期存活,那么 ThreadLocalMap也不会被销毁,导致 value导致累积大量无效 Entry,一直占用内存
1.3解决方法

使用完 ThreadLocal 后手动调用 remove()

threadLocal.remove(); // 清理当前线程的 ThreadLocal 变量

这样 ThreadLocalMap 就会手动删除 Entry,避免 value 残留。

2.线程池复用导致数据污染

线程池会复用线程,导致 ThreadLocal 变量未清理时,被下一个任务访问:

ExecutorService executor = Executors.newFixedThreadPool(2);
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);

for (int i = 0; i < 5; i++) {
    executor.submit(() -> {
        threadLocal.set((int) (Math.random() * 100));
        System.out.println(Thread.currentThread().getName() + " -> " + threadLocal.get());
    });
}

如果不 remove(),可能会导致脏数据问题。


为什么 ThreadLocal 是弱引用?

1.如果 key 是强引用

假设 ThreadLocalMap.Entry 采用强引用存储 ThreadLocal<?>

static class Entry {  
    ThreadLocal<?> key;  // 改为强引用
    Object value;  

    Entry(ThreadLocal<?> k, Object v) {  
        key = k;  
        value = v;  
    }  
}

那么,即使开发者手动置 ThreadLocal 变量为 null,它仍然无法被 GC 回收

ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set(new Object());

threadLocal = null; // 置为 null,开发者期望 GC 回收 ThreadLocal
System.gc(); // 但由于 key 仍然是强引用,GC 不会回收 ThreadLocal

此时:

  1. ThreadLocalMap仍然持有 ThreadLocal 的强引用,即使 threadLocal = null,它仍然存活在 ThreadLocalMap 里,无法被 GC。
  2. 长期运行的线程(如线程池)会一直持有 ThreadLocal,导致内存泄漏

根本原因

  1. ThreadLocalMap 的生命周期 == 线程的生命周期。
  2. 如果线程是长期存活的(如线程池),那么 ThreadLocalMap 也不会被销毁
  3. 即使 ThreadLocal 变量已经超出作用域,仍然有强引用,无法被回收,导致内存泄漏

2.采用弱引用,解决内存泄漏

JDK 设计者为了避免 ThreadLocalMap 内存泄漏,采用了 WeakReference<ThreadLocal<?>>

static class Entry extends WeakReference<ThreadLocal<?>> {  
    Object value;  
    Entry(ThreadLocal<?> k, Object v) {  
        super(k);  // key 使用弱引用
        value = v;  
    }  
}
  1. ThreadLocal<?>弱引用,当开发者不再使用 ThreadLocal 变量时,GC 会自动回收它
  2. ThreadLocal 被回收后,Entry.key 变为 null,但 value 仍然存在(强引用)。
  3. ThreadLocalMap** 在下次 set()/getEntry() 操作时会清理 key == nullEntry,确保 value 也被释放**。

3.为什么 value 不是弱引用?

如果 value 也是 WeakReference,那么可能会导致 ThreadLocal.get()返回 null,影响正常业务逻辑:

threadLocal.set(new Object()); // 假设 value 也是弱引用
System.gc(); 
threadLocal.get(); // 可能返回 null,因为 value 也被回收了

这样 ThreadLocal无法正常使用,导致业务代码异常。因此:

  1. key使用弱引用,允许 ThreadLocal 被 GC 回收,防止内存泄漏。
  2. value使用强引用,保证业务逻辑正常运行,避免 get() 返回 null

4.正确的使用方式

  1. 建议将ThreadLocal变量定义为static类型。这样设置可以延长ThreadLocal实例的生命周期,因为存在对它的强引用,从而确保了ThreadLocal对象不会被垃圾回收机制过早地清理掉。这种做法有助于在任何时候都能够通过ThreadLocal持有的弱引用来访问到其内部Entry中的值,并及时调用remove()方法来清除这些值,进而有效避免内存泄漏问题的发生。
  2. 在每次使用完ThreadLocal后,务必显式调用其remove()方法以清空存储的数据。这一措施对于预防潜在的内存泄漏至关重要。

ThreadLocal 与 Synchronized 的区别

  1. 相同点: ThreadLocal 和 Synchronized 都是用于处理多线程并发问题。
  2. 不同点(数据访问):
    1. ThreadLocal 适合“每个线程独立的变量“,为每个线程都提供一个副本,保证每个线程访问数据的隔离。
    2. Synchronized 适合“多个线程共享同一个变量”,使用锁机制,保证数据在某一时刻只能被一个线程访问。

应用场景

  1. 数据库连接管理(每个线程独立 Connection
  2. 用户身份信息(每个线程存储当前用户 Session
  3. 日志追踪(每个请求独立的 Trace ID
  4. 线程安全的工具类(如 SimpleDateFormat
  5. 等等

1.用户身份信息

这段代码实现了一个Spring MVC的拦截器(HandlerInterceptor),主要用于处理用户登录状态的验证。具体来说,它通过检查请求中的Token来判断用户是否已经登录,并将登录用户的信息存储在ThreadLocal中,以便在当前线程的后续处理中使用。

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    public static ThreadLocal<LoginUser> threadLocal = new ThreadLocal<>();

    /**
     * 处理请求前拦截
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 前后端分离会有option刺探请求,查看网络是否正常
        if (HttpMethod.OPTIONS.toString().equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpStatus.NO_CONTENT.value());
            return true;
        }

        // 获取token
        String accessToken = request.getHeader("token");
        if (!StringUtils.isNotBlank(accessToken)) {
            // 有些情况,请求头中token可能为空,就从参数中获取token
            accessToken = request.getParameter("token");
        }

        if (StringUtils.isNotBlank(accessToken)) {
            Claims claims = JWTUtil.checkJWT(accessToken);
            if (claims == null) {
                // 未登录
                CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
                return false;
            }

            // 用户已经登录
            Long accountNo = Long.valueOf(claims.get("account_no").toString());
            String headImg = claims.get("head_img").toString();
            String username = claims.get("username").toString();
            String mail = claims.get("mail").toString();
            String phone = claims.get("phone").toString();
            String auth = claims.get("auth").toString();

            LoginUser loginUser = LoginUser.builder()
                    .accountNo(accountNo)
                    .headImg(headImg)
                    .username(username)
                    .mail(mail)
                    .phone(phone)
                    .auth(auth)
                    .build();

            //request.setAttribute("loginUser", loginUser);

            // 将参数传入当前线程
            threadLocal.set(loginUser);

            return true;
        }
        CommonUtil.sendJsonMessage(response, JsonData.buildResult(BizCodeEnum.ACCOUNT_UNLOGIN));
        return false;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 当前线程完成后,移除
        threadLocal.remove();
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}

Reference

  1. 史上最全ThreadLocal 详解(一)
  2. ChatGPT

相关文章:

  • 智慧共享杆:城市智能化管理的 “多面手”
  • Linux 用户与组管理实战:经验分享与最佳实践
  • Oracle OCP认证是否值得考?
  • Unity 中实例化预制体的完整过程
  • 第7章 类与面向对象
  • linux性能监控的分布式集群 prometheus + grafana 监控体系搭建
  • 华为终端销售模式转型变革项目总体汇报方案(183页PPT)(文末有下载方式)
  • WordPress漏洞
  • 【Vue3】01-vue3的基础 + ref reactive
  • 大白话详细解读函数之柯里化
  • AI全天候智能助手,为您构建私人数据库
  • JVM的组成--运行时数据区
  • Vue的根路径为什么不能作为跳板跳转到其他页面
  • 潮流霓虹酸性渐变液体流体扭曲颗粒边缘模糊JPG背景图片设计素材 Organic Textures Gradients Collection
  • 如何提高自动化测试的覆盖率?
  • C++的多态性及其实现方式
  • conda 常用命令
  • 提升模型性能:数据增强与调优实战
  • 微信小程序:用户拒绝小程序获取当前位置后的处理办法
  • RabbitMQ的高级特性介绍(一)
  • 国家卫健委通报:吊销肖某医师执业证书,撤销董某莹四项证书
  • 习近平向多哥新任领导人致贺电
  • 落实中美经贸高层会谈重要共识,中方调整对美加征关税措施
  • 多家外资看好中国市场!野村建议“战术超配”,花旗上调恒指目标价
  • 日本广岛大学一处拆迁工地发现疑似未爆弹
  • 字母哥动了离开的心思,他和雄鹿队的缘分早就到了头