ThreadLocal 原理
每个线程内部维护着一个 ThreadLocalMap,这个 Map 的底层是由一个 Entry 数组实现的。Entry 中的 key 为弱引用类型,指向 ThreadLocal 本身,value 则是实际存储的变量 Object。
概述
ThreadLocal 的作用在于提供了「线程内的局部变量」,该变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
其他线程无法访问 ThreadLocal 内的变量,这样就隔离了多个线程间的资源,也不会出现线程安全问题。
Get 策略
1 | public T get() { |
- 获取当前线程。
- 获取当前线程的 ThreadLocalMap。
- 如果 ThreadLocalMap 不为空,以 ThreadLocal 的引用作为 key 来在 ThreadLocalMap 中获取对应的value e。
- if (e != null && e.get() == key),返回 e。
- ThreadLocalMap 为 null 或 value 为 null,则调用 setInitialValue() 方法返回初始值。
Set 策略
1 | public void set(T value) { |
- 获取当前线程。
- 获取当前线程的 ThreadLocalMap。
- ThreadLocalMap 不为 null,将 value 放入 key 为当前 ThreadLocal 的槽位中。
- 如果ThreadLocalMap 为 null,则新建一个 ThreadLocalMap 并将 value 放入 key 为当前 ThreadLocal 的槽位中。
Remove 策略
1 | public void remove() { |
- 获取当前线程的 ThreadLocalMap。
- 如果 ThreadLocalMap 不为 null,则清除。
- 注意这里的成员变量 threadLocals 其实就是 ThreadLocalMap。
扩容策略
1 | // threshold是数组长度的2/3 |
- 判断是否需要扩容,主要是 !cleanSomeSlots(i, sz) 比较难理解,cleanSomeSlots() 会从当前插入元素的位置往后扫描 table 中的元素并判断是否是 stale entry(即key 为 null 的 entry),如果找到则调用 expungeStaleEntry() 方法清理。如果找到 stale entry 则返回 true,否则返回 false。在expungeStaleEntry() 方法中,如果清除了 stale entry 就会导致 size—,这样情况下,set 新的 entry 时就可能不用扩容啦,所以才需要在这里判断一下。
- 调用 rehash() 方法,此时还未真正扩容,先全量清除stale entry,再判断size是否大于等于3/4扩容阈值才会真正扩容。
- 调用 resize() 扩容,不同于 ArrayList 中的 1.5 倍扩容,这里是按照 2倍 扩容,值得注意的是扩容的过程中也同时清理了 key 为 null 的 stale entry。
简单来说:
- 当数组元素数量达到了扩容阈值threshold(即数组容量的2/3)且找不到stale entry后,进入下一步。
- 执行全量清理 stale entry,如果清理后的元素数量大于3/4扩容阈值(即数组容量的1/2),进入下一步。
- 按照 2 倍容量执行真正的扩容。
哈希冲突问题
不同于 HashMap 中的链表法,ThreadLocalMap 采用的是开放地址法(线性探测)来解决哈希冲突问题。同时,不同于 HashMap 中的链表或红黑树,ThreadLocalMap 仅仅采用了数组来维护 Map。
注意到 ThreadLocal 中的一个魔数 HASH_INCREMENT = 0x61c88647,ThreadLocalMap 就是通过这个魔数来提高散列性以减少哈希碰撞的概率。
0x61c88647可以理解为黄金分割数(0.618)乘以2的32次方,它可以保证 nextHashCode 生成的哈希值,均匀的分布在2的N次方的数组里,且小于2的32次方。
但是尽管该方法可以有效减少哈希冲突的概率,但并不能完全避免哈希冲突,如果哈希冲突出现, ThreadLocal 会通过线性探测(步长加1或减1,寻找下一个相邻的位置)来查找下一个可用的槽位存放新的 entry。
这里的细节是如果槽位的 key 存放的是同一个对象,则覆盖 value,如果槽位存放的 key 为 null,则会替换它的位置。
不采用线程 id 来作为 ThreadLocalMap key的原因
因为一个线程中可以有多个 ThreadLocal 对象,所以 ThreadLocalMap 中可以有多个键值对,而如果使用线程 id 作为 key,就只有一个键值对。
Entry 采用 WeakReference 的原因
作者在注释中说道:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
为了保证在大量生命周期长对象存在时Map的高效可利用性,使用了WeakReferences来作为key。
如果 key 使用的是强引用,当引用 ThreadLocal 的对象被 GC 回收时,如果 ThreadLocalMap 还持有 ThreadLocal 的强引用且没有被手动删除,ThreadLocal 就永远不会被回收,导致 Entry 内存泄漏。
而如果 key 使用的是弱引用的话,当引用 ThreadLocal 的对象被 GC 回收时,由于 ThreadLocalMap 的 key 为弱引用类型,即使没有手动删,key 也会被 GC 回收。这样 value 在下一次 ThreadLocalMap 调用set(),get(),remove() 方法时就会被清除。
ThreadLocal 的内存泄漏问题
内存泄漏(Memory leak)和内存溢出(Out of memory)的区别:
- 内存溢出是指程序在申请新的内存时,没有足够的内存空间供其使用。
- 内存泄漏是指程序申请完内存后,无法释放已申请的内存空间,不再使用的对象或者变量仍占内存空间。
- Memory leak会最终会导致Out of memory!
导致 ThreadLocal 内存泄漏的原因:
ThreadLocalMap 中使用 ThreadLocal 的弱引用作为 key,在没有外部对象强引用时,弱引用的 key 在 GC 中会被回收,而 value 不会回收,从而导致内存泄漏。
这种情况下,key 为 null 的 Entry 的 value 会一直存在一条强引用链:
ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value
即内存泄漏的根源在于没有及时删除 key 对应的 value,而不是采用了 WeakReference。
实际上作者已经在源码中做出了优化来避免该问题,在源码中处处可见删除 stale entry 的操作。
在 ThreadLocal的 get()、set()、remove() 方法调用时都会有额外的操作来清除 ThreadLocalMap 中的 stale entry。但如果一直没有执行 get()、set()、remove() 这些方法,还是避免不了内存泄漏,所以 remove() 方法就变得十分重要了。
如何避免内存泄漏:
调用 remove() 方法,将 Entry 和 Map 的引用关系移除,这样整个 Entry 对象在 GC Roots 分析后就变成不可达了,下次 GC 的时候就可以被回收。
手动将 value 设为 null,让 JVM 进行回收。
源码阅读
1 |
|