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函数的流程大概为:
- 首先从ThreadLocal的找到索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;
- 如果e为null或者key不一致则向数组table的下一个位置查询,如果发现相等,则返回对应的Entry。
- 否则,如果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