• 基础 | 并发编程 - [ThreadLocal]


    §1 作用

    • 是成员变量的线程本地化形态
    • 使静态变量的值与线程绑定,即隔离了其他线程对当前线程中变量的操作

    在理解时,不宜将 ThreadLocal 理解为横跨若干线程、存储并管理不同线程某些成员值的容器

    • 因为 ThreadLocal 本身没有存储功能,提供存储功能的是 ThreadLocalMap
    • ThreadLocalMap 并不是共用的,每个线程持有自己的 ThreadLocalMap
    • ThreadLocal 仅相当于对各个线程各自的 ThreadLocalMap 的统一操作面板
    • 将它理解为成员变量的线程本地化形态更加贴切

    §2 API

    方法作用备注示例
    get()获取当前线程的变量值获取变量值的快照
    set()设置当前线程的变量值
    remove()移除当前线程的变量值使用后应该在 finally 中移除,否则尤其是线程池场景容易造成内存泄漏
    withInitial()初始化当前线程的变量值静态方法,默认的初始值是 nullThreadLocal.withInitial(()->0)
    ThreadLocal<Integer> count = ThreadLocal.withInitial(()->0);
    
    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        for(int i=0;i<5;i++){
            new Thread(()->{
                try {
                    demo.count.set(demo.count.get()+ new Random().nextInt(10));
                    System.out.println(Thread.currentThread().getName()+ " | " + demo.count.get());
                } finally {
                    demo.count.remove(); // 用完即删
                }
            },String.valueOf(i)).start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    §3 ThreadThreadLocalThreadLocalMap

    每个 Thread 中都有一个 ThreadLocalMap
    在这里插入图片描述
    ThreadLocalMapThreadLocalMap.Entry 组成
    在这里插入图片描述
    ThreadLocalMap.EntryThreadLocal 的内部类,用于存储 ThreadLocal 的弱引用value 的映射
    或者说,ThreadLocalMap.Entry 就是一种带有 value 的弱引用 WeakReference>
    在这里插入图片描述
    通过 ThreadLocal 存取值时,其实是对 ThreadLocalMap(里的 ThreadLocalMap.Entry ) 进行操作
    get() 为例

    • 获取当前线程的 ThreadLocalMap
    • 按获取 hash 表落点(对长度取余)的方式获取对应的 ThreadLocalMap.Entry
      在这里插入图片描述
      在这里插入图片描述

    总结:

    • Thread 持有各自独立的 ThreadLocalMap
    • ThreadLocalMap 中存储了当前线程的各个线程本地化变量
      ThreadLocal 的弱引用和 value 的映射
      ThreadLocal 本身并不存储值,存值的是ThreadLocalMap
    • 同一个 ThreadLocal 在不同 ThreadLocalMap 中映射不同的值


    示例
    注意各个对象的 id

    ThreadLocal<Integer> count = ThreadLocal.withInitial(()->0);
    ThreadLocal<Integer> num = ThreadLocal.withInitial(()->0);
    
    public static void main(String[] args) {
        ThreadLocalDemo demo = new ThreadLocalDemo();
        new Thread(()->{
            demo.count.set(10);
            demo.num.set(20);
            System.out.println(demo.count.get()+demo.num.get());
        }).start();
    
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) { e.printStackTrace(); }
        new Thread(()->{
            demo.count.set(1);
            demo.num.set(2);
            System.out.println(demo.count.get()+demo.num.get());
        }).start();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    在这里插入图片描述

    在这里插入图片描述

    §3 内存泄漏

    为什么会有内存泄漏
    先暂时忽略 ThreadLocalMap.Entry 上的弱引用,只考虑几个类的实例之间的引用关系,如下图所示

    • 两个线程 A、B,各自持有各自的 ThreadLocalMap
    • 每个 ThreadLocalMap 可能持有多个 ThreadLocalMap.Entry
    • 每个 ThreadLocalMap.Entry 又引用各自的 ThreadLocal 和 值
      不同 ThreadLocalMap.Entry 上引用的 ThreadLocal 又可能是同一个
      在这里插入图片描述

    线程销毁 时,线程与它栈帧中的 ThreadLocalMapEntry 都会回收
    Entry 引用的 ThreadLocal 和 值没有额外引用(比如其他线程,或声明着 ThreadLocal 的对象),则也会被回收

    但在 线程复用的场景(比如线程池),线程不会被销毁,会出现两种 内存泄漏

    • ThreadLocalThreadLocalMap.Entry 持有导致
    • key==null 的 ThreadLocalMap.Entry 导致

    ThreadLocalThreadLocalMap.Entry 持有导致的内存泄漏

    线程复用场景中,上述各个对象都不会被回收
    这使得即使声明着 ThreadLocal 的对象都已经被回收了,ThreadLocal 也不不能被回收
    因为它依然可以存在于各个线程的 ThreadLocalMap

    JDK 为了解决这个问题,将 Entry 设计成弱引用 key,如下图
    在这里插入图片描述
    EntryThreadLocal 的引用被定义为弱引用,当发生 GC 时,Entry 的 key 会被置空(null)
    若此时,原本作为 key 的 ThreadLocal 未被其他有效引用所引用的 ThreadLocal 对象会被回收,完美的解决了此问题

    key==null 的 ThreadLocalMap.Entry 导致的内存泄漏

    但是,key==null 的 Entry 依然被 ThreadLocalMap 持有
    这相当于标记了这个 Entry 为腐败Entry(stale entry),变得不可访问,但它和它引用的 value 依然占用内存
    JDK 为了解决这个问题,为 ThreadLocal 提供了两种方案

    • 自动回收,即 replaceStaleEntry() / expungeStaleEntry() 方法
      这些方法会在 set() / get() 的过程中发现 null 值的 key 后自动调用以清理腐败 Entry
      这可以解决大部分内存泄漏问题,但不能保证100%(比如 value 存了个很大的东西,然后在没有 set() / get() 过 )
      在这里插入图片描述

    • 手动回收,remove() 方法,如下图
      remove() 方法会直接对 key 进行 expungeStaleEntry()
      使用完 ThreadLocal 后,应该调用此方法以做到实时清理 ThreadLocalMap ,见上文的 例子
      在这里插入图片描述
      在这里插入图片描述

    题外话:为什么 Entry 的 value 不能通过弱引用解决
    因为 value 有可能出现只被 Entry 引用的情况,这回导致在还没有使用完时,因 GC 丢失值
    Entry 的 key 是因为它至少被声明 ThreadLocal 的对象强引用着,对象销毁才意味着它们已经没用了

    总结
    使用 ThreadLocal 造成内存泄漏的场景通常是因为

    • 线程复用的场景,比如线程池
    • 没有使用或正确使用 ThreadLocalremove()

    §4 最佳实践

    • 用 static 修饰 ThreadLocal
      • 不强制,没有强制必要
      • 好处是可以在类装载时只开辟一块空间,而不需要随对象生灭重复开辟
    • 通过 withInitial() 方法初始化 ThreadLocal
      • 默认的初始值是 null
      • 可能不满足业务场景
      • 初始化后没 set() 直接 get() 后容易出现空指针异常
    • 使用后调用 remove() 方法
      • 线程复用场景下防止内存泄漏
      • 线程复用场景下防止得到线程上次被使用时保存的值,出现 bug
    • 避免向 ThreadLocalset() 共享对象(虽然线程隔离了,但隔离中的还是同一个东西的投影)
      • ThreadLocal 并不能完美的隔离所有变量
      • 对各种基础类型可以通过 ThreadLocal 变相实现线程安全
      • 但引用型变量,尤其是常见的不安全容器,则依然可能会造成多个线程不安全的访问它们
        比如在多个线程中 set() 同一个 HashMap
  • 相关阅读:
    Vue项目的详细目录结构解析
    【Python】PyGithub+jinja2 生成Github项目简易海报
    计算机网络概念入门(十一)
    【考研英语语法】语篇标记
    数组模拟队列进阶版本——环形队列(真正意义上的排队)
    隧道未来如何发展?路网全息感知,颠覆公路交通安全史
    【java期末复习题】第2章 Java语言的基本语法
    2022年--软件设计师下午题高频考点技巧总结
    Flutter教程之Dart 和 Flutter 的工厂设计模式
    痞子衡嵌入式:理解i.MXRT中FlexSPI外设lookupTable里配置访问行列混合寻址Memory的参数值
  • 原文地址:https://blog.csdn.net/ZEUS00456/article/details/126884130