ThreadLocal 及其内存泄漏 详解

前言

本章用于记录一下ThreadLocal相关的一些理解和分享,本文并非完全原创,只是自己结合前辈们的理解下理解思路记录,方便自己学习巩固,如果你在仔细阅读这篇文章之后感觉有帮助,请不要吝啬点赞关注哦~

ThreadLocal是什么?

ThreadLocal是线程私有的一个实现,它内部是一个map,这个map是ThreadLocalMap,它的key是ThreadLocal,value是我们要存的线程变量的副本。ThreadLocalMap是ThreadLocal的一个静态内部类,并且是继承于WeakReference<ThreadLocal<?>>(就是说map的键是一个弱引用)的,WeakReference是Java中弱引用的一种标记,这个和内存泄露有点关系,但是并不是内存泄露的原因,继续往下看 --> 也就是说ThreadLocalMap是定义在ThreadLocal中,但是引用是在Thread中。

查看Thread类的属性会有一个(ThreadLocal.ThreadLocalMap threadLocals = null;)。因此ThreadLocalMap与Thread的生命周期是一样长的。

这里需要注意的是:
ThreadLocal不是用来解决多线程并发访问异常的。因为每一个线程在ThreadLocalMap中存储的共享的对象value都不是同一个。而是线程对象的一个副本。

每一个ThreadLocal对象是如何区分的?

查看ThreadLocal源码,可以看到:

	//java提供的,可以用原子方式更新的 int值的类。
    private static AtomicInteger nextHashCode = new AtomicInteger();
    private static final int HASH_INCREMENT = 0x61c88647;
    private final int threadLocalHashCode = nextHashCode();

    private static int nextHashCode() {
        //原子性加一
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

对于每一个ThreadLocal对象,都有一个final的int值threadLocalHashCode;nextHashCode 是AtomicInteger类型并且是static修饰的,全局唯一,每一次加一之后的值仍然可用,并且保证原子性。所以,每一个线程的ThreadLocal对象都有唯一的threadLocalHashCode值。

关于内存泄露的问题:

刚才说到ThreadLocalMap的key是ThreadLocal实例的弱引用,如果ThreaLocal对象没有一个强引用,那么当gc时,ThreadLocal对象会被回收,ThreadLocalMap内Entry的key就变成null。而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉(ThreadLocalMap与Thread的生命周期是一样长的,Thread销毁,ThreadLocalMap的生命周期也就到头了)。

但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而ThreadLocalMap与Thread的生命周期是一样长的,如果线程迟迟没有死亡,那么ThreadLocalMap中这个value就永远无法回收,造成内存泄漏。

那为什么ThreadLocalMap内Entry的key使用弱引用而不是强引用??

当ThreadLocalMap的key为强引用时,ThreadLocal在使用完被回收时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal就不会被回收,导致Entry内存泄漏。

当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。
ThreadLocal被回收之后,ThreadLocalMap就出现了key为null,而value还存在着强引用的情况,但是使用弱引用可以多一层保障来保证弱引用ThreadLocal不会内存泄漏:ThreadLocalMap对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。

从源码角度来解释一下get方法:
ThreadLocalMap的getEntry函数的流程大概为:

  1. 首先从ThreadLocal的找到索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
  2. 如果e为null或者key不一致则向数组table的下一个位置查询,如果发现相等,则返回对应的Entry。
  3. 否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。

set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

但是这些操作的前提是调用set方法或者getEntry和remove()方法,所以JAVA官方推荐将ThreadLocal定义为static全局唯一,避免丢失ThreadLocal强引用,就能保证随可以调用ThreadLocal的remove方法去清除。并且在使用完ThreadLocal时,及时调用它的的remove方法清除数据。

ThreadLocal正确的使用方法

  • 每次使用完ThreadLocal都调用它的remove()方法清除数据。
  • 将ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值,进而清除掉 。

感谢

https://juejin.im/post/5e184276e51d4557e86e8afd

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值