线程私有变量 ThreadLocal 详解
本文已收录至 Github,推荐阅读 👉 Java随想录
微信公众号:Java 随想录
CSDN: 码农BookSea
烈火试真金,逆境试强者。——塞内加
什么是 ThreadLocal
首先看下 ThreadLocal 的使用示例:
执行结果如下
我们从 Thread
类讲起,在 Thread
类中有维护两个 ThreadLocal.ThreadLocalMap
对象,分别是:threadLocals
和inheritableThreadLocals
。
初始它们都为 null,只有在调用 ThreadLocal
类的 set 或 get 时才创建它们。ThreadLocalMap 可以理解为线程私有的 HashMap。
ThreadLoalMap 是 ThreadLocal 中的一个静态内部类,类似 HashMap 的数据结构,但并没有实现 Map 接口。
ThreadLoalMap 中初始化了一个大小 16 的 Entry 数组,Entry 对象用来保存每一个 key-value 键值对。key 是 ThreadLocal 对象。
Entry 用来保存数据 ,而且还是继承的弱引用。在 Entry 内部使用 ThreadLocal 作为 key,使用我们设置的 value 作为 value。
ThreadLocal 原理
set()方法
当我们调用 ThreadLocal 的 set()
方法时实际是调用了当前线程的 ThreadLocalMap 的 set() 方法。ThreadLocal 的 set() 方法中,会进一步调用Thread.currentThread()
获得当前线程对象 ,然后获取到当前线程对象的 ThreadLocal,判断是不是为空,为空就先调用creadMap()
创建再set(value)
创建 ThreadLocalMap 对象并添加变量。不为空就直接set(value)
。
这种保证线程安全的方式称为线程封闭
。线程只能看到自己的 ThreadLocal 变量。线程之间是互相隔离的。
get()方法
其中 get()方法用来获取与当前线程关联的 ThreadLocal 的值,如果当前线程没有该 ThreadLocal 的值,则调用 initialValue 函数获取初始值返回,所以一般我们使用时需要继承该函数,给出初始值(不重写的话默认返回 Null)。
主要有以下几步:
获取当前的 Thread 对象,通过 getMap 获取 Thread 内的 ThreadLocalMap
如果 map 已经存在,以当前的 ThreadLocal 为键,获取 Entry 对象,并从从 Entry 中取出值
否则,调用 setInitialValue 进行初始化。
我们可以重写initialValue()
,设置初始值。
remove()方法
最后一个需要探究的就是 remove 方法,它用于在 map 中移除一个不用的 Entry。也是先计算出 hash 值,若是第一次没有命中,就循环直到 null,在此过程中也会调用 expungeStaleEntry 清除空 key 节点。代码如下:
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。出现内存泄漏的问题。
在执行 ThreadLocal 的 set、remove、rehash 等方法时,它都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,所以它就会把对应的 value 置为 null,这样,value 对象就可以被正常回收了。但是假设 ThreadLocal 已经不被使用了,那么实际上 set、remove、rehash 方法也不会被调用,与此同时,如果这个线程又一直存活、不终止的话,那么刚才的那个调用链就一直存在,也就导致了 value 的内存泄漏。
ThreadLocal 的 Hash 算法
ThreadLocalMap
类似 HashMap,它有自己的 Hash 算法。
HASH_INCREMENT
这个数字被称为斐波那契数 也叫 黄金分割数,带来的好处就是 hash
分布非常均匀。
每当创建一个ThreadLocal
对象,这个ThreadLocal.nextHashCode
这个值就会增长 0x61c88647
。
讲到 Hash 就会涉及到 Hash 冲突,跟 HashMap 通过链地址法不同的是,ThreadLocal 是通过线性探测法/开放地址法来解决 hash 冲突。
ThreadLocal 1.7 和 1.8 的区别
ThreadLocal 1.7 版本的时候,entry 对象的 key 是 Thread。
1.8 版本 entry 的 key 是 ThreadLocal。
1.8 版本的好处 :当 Thread 销毁的时候,ThreadLocalMap 也会随之销毁,减少内存的使用。因为 ThreadLocalMap 是在 Thread 里面的,所以只要 Thread 消失了,那 ThreadLocalMap 就不复存在了。
ThreadLocal 的问题
ThreadLocal 内存泄露问题
在 ThreadLocalMap 中的 Entry 的 key 是对 ThreadLocal 的 WeakReference
弱引用,而 value 是强引用。当 ThreadLocalMap 的某 ThreadLocal 对象只被弱引用,GC 发生时该对象会被清理,此时 key 为 null,但 value 为强引用不会被清理。此时 value 将访问不到也不被清理掉就可能会导致内存泄漏。
注意构造函数里的第一行代码 super(k),这意味着 ThreadLocal 对象是一个弱引用
因此我们使用完 ThreadLocal 后最好手动调用 remove()
方法。但其实在 ThreadLocalMap 的实现中以及考虑到这种情况,因此在调用 set()
、get()
、remove()
方法时,会清理 key 为 null 的记录。
为什么使用弱引用而不是强引用?
为什么采用了弱引用的实现而不是强引用呢?
注释上有这么一段话:为了协助处理数据比较大并且生命周期比较长的场景,hash table 的条目使用了 WeakReference 作为 key。
所以,弱引用反而是为了解决内存存储问题而专门使用的。
实际上,采用弱引用反而多了一层保障,ThreadLocal 被清理后 key 为 null,对应的 value 在下一次 ThreadLocalMap 调用 set、get,就算忘记调用 remove 方法,弱引用比强引用可以多一层保障。
所以,内存泄露的根本原因是是否手动清除操作,而不是弱引用。
ThreadLocal 父子线程继承
异步场景下无法给子线程共享父线程的线程副本数据,可以通过 InheritableThreadLocal
类解决这个问题。
它的原理就是子线程是通过在父线程中调用 new Thread()
创建的,在 Thread 的构造方法中调用了 Thread的init
方法,在 init
方法中父线程数据会复制到子线程(ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
)。
代码示例:
但是我们做异步处理都是使用线程池,线程池会复用线程会导致问题出现。我们可以使用阿里巴巴的 TTL 解决这个问题。
https://github.com/alibaba/transmittable-thread-local
如果本篇博客有任何错误和建议,欢迎给我留言指正。文章持续更新,可以关注公众号第一时间阅读。
评论 (1 条评论)