写点什么

ThreadLocal 源码解析:巧用弱引用解决内存泄漏问题

作者:程序员小毕
  • 2022 年 9 月 13 日
    湖南
  • 本文字数:3080 字

    阅读完需:约 10 分钟

ThreadLocal源码解析:巧用弱引用解决内存泄漏问题

一丶 ThreadLocal 结构



每一个 Thread 对象都有一个名为threadLocals类型为ThreadLocal.ThreadLocalMap的属性,ThreadLocal.ThreadLocalMap对象内部存在一个Entry数组,其中存储的 Entry 对象 key 是ThreadLocal,value 便是我们绑定在线程上的值。ThreadLocal 可以做到线程隔离是由于每一个线程对象持有一个 ThreadLocalMap,每一个线程对 ThreadLocalMap 的处理是互不影响的。之所以持有的是 ThreadLocalMap,是线程可能使用多个 ThreadLocal 存储数据,比如在 Spring 事务同步管理器中TransactionSynchronizationManager包含三个 ThreadLocal 对象,一个管理事务相关资源,一个管理当前事务需要回调的同步接口,一个管理事务名称,三个 ThreadLocal 对象对应着当前Thread持有的ThreadLocal.ThreadLocalMap中 Entry 数组的的三个 Entry

二丶源码学习

1.set(T value)——向 ThreadLocal 中设置值



拿到当前线程Thread.currentThread()这是一个 Native 方法,getMap方法便是获取线程中的ThreadLocal.ThreadLocalMap threadLocals属性,包装成方法便于子类重写覆盖。如果当前线程的ThreadLocalMap 不为空那么向ThreadLocalMap 中设置值,反之调用createMap初始化 map。通常第一次设置值的时候ThreadLocalMap为空。

2.createMap(Thread t, T firstValue)——为线程初始化 ThreadLocalMap



方法很简单直接调用 ThreadLocalMap 的构造函数,在研究此构造函数之前我们先看下 ThreadLocalMap 的结构,其包含一个Entry数组,其中 Entry 继承了WeakReference



2.1 为什么这里 Entry 保存 ThreadLocal 类型的 key 使用弱引用:

我们知道弱引用具备的性质:在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用指向的对象,不管当前内存空间足够与否,都会回收它的内存。这里使用弱应用是为了防止 oom,如果 ThreadLocal 作为 Key 不使用弱引用,如果根据可达性算法此 ThreadLocal 已经无法和 GCRoot 关联(没有任何强引用指向当前 ThreadLocal),但是当前线程并没有结束,可以通过当前线程关联到其threadLocals属性对应的ThreadLocalMap,再关联到 Entry 中的 ThreadLocal 对象,这时候 ThreadLocal 将永远无法被回收。



这里我们给出一个启动线程执行死循环,再死循环中创建 ThreadLocal 并 set,这段代码执行并不会发生 OOM,原因是 ThreadLocal 是被弱引用指向,在发生 GC 的时候会被回收。



这里应该还有一个问题,虽然 ThreadLocal 被回收了,但是 Entry 数组一直在塞入 Entry,回收之后就相当于 Entry 的 key 为 null,value 存在值,那么为什么不会 oom 昵,原因是往 ThreadLocalMap 中塞入元素的时候,会删除掉过时(指 Entry 中的 key 弱引用持有的 ThreadLocal 为 null)的元素。

2.2 ThreadLocalMap 构造方法



这里使用到ThreadLocal.threadLocalHashCode此值由nextHashCode方法生成,其使用AtomicInteger原子类生成



其中firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1是为了让 hash 分布均匀减少 hash 冲突(类似于 HashMap 中高位低位进行异或),至于为什么使用0x61c88647我没有深究。

setThreshold方法是使用属性threshold记录当前 Entry 数组长度的2/3作为扩容阈值,扩容逻辑后续进行解析。

3.ThreadLocalMap#set(ThreadLocal<?> key, Object value) 存入数据

set 方法的逻辑可以分作两部分:1.使用开放地址法找到合适的位置存储数据,2.向数组中放入新 Entry,有需要的话扩容

3.1.使用开放地址法找到合适的位置存储数据



第一个 if 意味着是相同的 ThreadLocal,类似于 HashMap put 相同 key 的元素多次,后续的后覆盖前面的,这里也一样,进行覆盖。

第二个 if 意味着,原来霸占 Entry 数组位置的 ThreadLocal 弱应用持有的 ThreadLocal 被回收了会调用replaceStaleEntry覆盖

3.2 向数组中放入新 Entry,有需要的话扩容

上面 for 循环进行的条件是e != null,e 是 Entry 数组中元素,那么结束 for 循环,除了成功覆盖原有元素的还有找到一个可以使用的位置



这里扩容的条件有两个cleanSomeSlots删除过期的条目失败,且 当前 Entry 数组存入元素大于扩容阈值



扩容代码如下,遍历所有的元素,如果已经被回收了那么将 value 也置为 null,如果没有被回收那么将元素拷贝到新的位置



这里为什么要将 value 也置为空昵

首先 ThreadLocal 的 key 已经被回收了,这时候调用者没办法拿到被回收 key 对应的 value,所有置为 null 是不会影响到使用的。

关键的是Help the GC的注释,置为 null 可以帮助 jvm 进行 GC,我们首先看下如下方法



此方法也不会发生 OOM



理论直接将被回收 Entry 位置的元素置为 null,这时候也是无法通过 GC Root 应用到 Entry,自然也无法引用到 String 对象,直接置为 null 也是相应的目的

这里扩容复制元素没有像 HashMap 进行低位不变,高位增加一个数组长度的操作,还是使用开放地址法找到合适的位置。

4.ThreadLocal#get()——获取和当前线程绑定在此 ThreadLocal 上的值



这部分代码分为两部分看:

4.1 获取 ThreadLocalMap 中的值

获取当前线程中的 ThreadLocalMap 属性,以当前 ThreadLocal 作为 key 获取到对应的值,具体获取的逻辑在ThreadLocalMap#getEntry方法



首先是对 Entry 数组的长度进行取模,获取当前 ThreadLocal 对应的位置,如果存在,且 Entry 中的 ThreadLocal 和当前入参的 ThreadLocal 相同(之所以需要这么判断是因为,hash 冲突后当前 ThreadLocal 会被放在后续的位置,只有二者的地址相同才能返回),那么返回。之所以判断e!=null可能是当前线程先删除再 get,这时候不判断会抛出空指针。



getEntryAfterMiss方法并不复杂,就是利用nextIndex找下一个位置,类似于 HashMap 中拉链法需要遍历链表一样,如果下一个位置为 null,说明当前 ThreadLocal 没有存储过,直接返回 null

4.2ThreadLocalMap 没有初始化,或者没有从 ThreadLocalMap 中获取到对应的值

这里会直接调用setInitialValue方法



其中initialValue()方法是给子类复写提供的方法,我们可以如下为 ThreadLocal 设置初始值



也可以使用 ThreadLocal 提供的静态工厂方法,如



使用此静态方法返回的是SuppliedThreadLocalinitialValue方法会调用传入的 Supplier,两种方法都可以自定义 ThreadLocal 没有设置值的时候返回的初始值

5.ThreadLocal#remove()



首先自然是获取当前线程的 ThreadLocalMap,如果初始化了才进行删除,然后调用ThreadLocalMap#remove方法,把当前 ThreadLocal 作为 key



删除过期条目的expungeStaleEntry方法,会将 Entry 数组中过期的条目(弱引用被回收,或者被删除的条目)置为 null。

三丶 InheritableThreadLocal 支持继承的 ThreadLocal

这里说的继承是指父线程往InheritableThreadLocal设置了值,然后父线程开启子线程,子线程的InheritableThreadLocal会拷贝其中的值



如上图,运行test5()方法的线程是 main 线程,首先向其中设置值parent,然后开启子线程,子线程运行直接使用 get 并打印出parent。具体原理是Thread的构造方法会拿到当前线程中的inheritableThreadLocals内容复制到子线程的inheritableThreadLocals



这里调用了ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)将返回值设置到创建线程的inheritableThreadLocals属性上



逻辑也很简单,遍历父线程中的 entry 元素,调用childValue方法,实现父 Entry 值映射成子 Entry 值(InheritableThreadLocal默认直接信息映射,如有需要可以覆盖childValue方法),然后使用开放地址法存到子线程中。

其中InheritableThreadLocal还重写了getMapcreateMap方法,二者都操作 Thread 中的inheritableThreadLocals属性



作者:Cuzzz

原文:https://www.cnblogs.com/cuzzz/p/16687535.html

如果感觉本文对你有帮助,点赞关注支持一下,想要了解更多 Java 后端,大数据,算法领域最新资讯可以关注我公众号【架构师老毕】私信 666 还可获取更多 Java 后端,大数据,算法 PDF+大厂最新面试题整理+视频精讲

用户头像

领取资料添加小助理vx:bjmsb2020 2020.12.19 加入

Java领域;架构知识;面试心得;互联网行业最新资讯

评论

发布
暂无评论
ThreadLocal源码解析:巧用弱引用解决内存泄漏问题_Java_程序员小毕_InfoQ写作社区