基础

为什么用ThreadLocal

避免线程共享,保证线程安全

使用案例

  1. public class ThreadLocaDemo {
  2. private static ThreadLocal<String> localVar = new ThreadLocal<String>();
  3. static void print(String str) {
  4. //打印当前线程中本地内存中本地变量的值
  5. System.out.println(str + " :" + localVar.get());
  6. //清除本地内存中的本地变量
  7. localVar.remove();
  8. }
  9. public static void main(String[] args) throws InterruptedException {
  10. new Thread(new Runnable() {
  11. public void run() {
  12. ThreadLocaDemo.localVar.set("local_A");
  13. print("A");
  14. //打印本地变量
  15. System.out.println("after remove : " + localVar.get());
  16. }
  17. },"A").start();
  18. Thread.sleep(1000);
  19. new Thread(new Runnable() {
  20. public void run() {
  21. ThreadLocaDemo.localVar.set("local_B");
  22. print("B");
  23. System.out.println("after remove : " + localVar.get());
  24. }
  25. },"B").start();
  26. }
  27. }
  28. A :local_A
  29. after remove : null
  30. B :local_B
  31. after remove : null

实现

  1. Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有 一个属于自己的ThreadLocalMap。<br /> ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,keyThreadLocal 身,valueThreadLocal的泛型值。 <br /> 每个线程在往ThreadLocal里设置值的时候,都是往自己的ThreadLocalMap里存,读也是以某个 ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。 <br /> ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25789321/1642761271240-50aba488-53da-4958-a703-1c6ae5cdd430.png#clientId=u7b1d2363-24bf-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=475&id=udc936848&margin=%5Bobject%20Object%5D&name=image.png&originHeight=697&originWidth=840&originalType=binary&ratio=1&rotation=0&showTitle=false&size=232219&status=done&style=none&taskId=u935f4a75-1764-44c4-b231-ab0f01fea1a&title=&width=572) <br /> <br /> <br /> <br />**为什么要这么设计呢**?

ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。Java 的这种实现方案虽然看上去复杂一些,但是更加安全。

源码解析

BF7BF5CA-7C68-442C-818E-58E62458D80B.png


一些补充

为什么用弱引用?

Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。(弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉)

内存泄漏问题

261C264B-A1BC-458E-A130-B85083CCAB86.png
既然有内存泄漏问题,那如何正确使用 ThreadLocal 呢?
其实很简单,既然 JVM 不能做到自动释放对 Value 的强引用,那我们手动释放就可以了。如何能做到手动释放呢?估计你马上想到 try{}finally{}方案了,这个简直就是手动释放资源的利器。示例的代码如下,你可以参考学习。

  1. ExecutorService es;
  2. ThreadLocal tl;
  3. es.execute(()->{
  4. //ThreadLocal增加变量
  5. tl.set(obj);
  6. try {
  7. // 省略业务逻辑代码
  8. }finally {
  9. //手动清理ThreadLocal
  10. tl.remove();
  11. }
  12. });

InheritableThreadLocal 与继承性

过 ThreadLocal 创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过 ThreadLocal 创建了线程变量 V,而后该线程创建了子线程,你在子线程中是无法通过 ThreadLocal 来访问父线程的线程变量 V 的。
如果你需要子线程继承父线程的线程变量,那该怎么办呢?
其实很简单,Java 提供了 InheritableThreadLocal 来支持这种特性,InheritableThreadLocal 是 ThreadLocal 子类,所以用法和 ThreadLocal 相同。

  1. //new Thread().init()方法中代码 ,详见
  2. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  3. this.inheritableThreadLocals =
  4. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

不过,我完全不建议你在线程池中使用 InheritableThreadLocal,不仅仅是因为它具有 ThreadLocal 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 InheritableThreadLocal,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命。

参考

1、30 | 线程本地存储模式:没有共享,就没有伤害-极客时间
2、《面试小抄 by 库森》