基础 | 并发编程 - [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();
    }
}

§3 ThreadThreadLocalThreadLocalMap

每个 Thread 中都有一个 ThreadLocalMap
在这里插入图片描述
ThreadLocalMapThreadLocalMap.Entry 组成
在这里插入图片描述
ThreadLocalMap.EntryThreadLocal 的内部类,用于存储 ThreadLocal 的弱引用value 的映射
或者说,ThreadLocalMap.Entry 就是一种带有 value 的弱引用 WeakReference<ThreadLocal<?>>
在这里插入图片描述
通过 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();
}

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

§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
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值