7

ThreadLocal源码学习笔记 - Cuzzz

 2 years ago
source link: https://www.cnblogs.com/cuzzz/p/16687535.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

系列文章目录和关于我

一丶ThreadLocal结构

image-20220912155015204

每一个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中设置值

image-20220912162732491

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

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

image-20220912163337105

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

image-20220912164142408

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

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

image-20220912171424317

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

image-20220912172845742

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

2.2 ThreadLocalMap构造方法

image-20220912175632569

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

image-20220912175855697

其中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.使用开放地址法找到合适的位置存储数据

image-20220912182943738

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

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

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

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

image-20220912183722977

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

image-20220912184341958

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

image-20220912185926597

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

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

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

image-20220912190508878

此方法也不会发生OOM

image-20220912190751128

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

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

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

image-20220912191809282

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

4.1获取ThreadLocalMap中的值

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

image-20220912192702025

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

image-20220912193722607

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

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

这里会直接调用setInitialValue方法

image-20220912194221872

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

image-20220912194358814

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

image-20220912194441817

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

5.ThreadLocal#remove()

image-20220912194843907

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

image-20220912195055884

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

三丶InheritableThreadLocal支持继承的ThreadLocal

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

image-20220912205839246

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

image-20220912213255396

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

image-20220912214050852

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

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

image-20220912221812268

本文作者:Cuzzz

本文链接:https://www.cnblogs.com/cuzzz/p/16687535.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK