首页 > 编程语言 >ThreadLocal源码学习笔记

ThreadLocal源码学习笔记

时间:2023-05-26 21:24:41浏览次数:51  
标签:ThreadLocalMap 笔记 ThreadLocal 源码 线程 key Entry null

系列文章目录和关于我

一丶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

  转:https://www.cnblogs.com/cuzzz/p/16687535.html

标签:ThreadLocalMap,笔记,ThreadLocal,源码,线程,key,Entry,null
From: https://www.cnblogs.com/blogzero/p/17435838.html

相关文章

  • 极光笔记 | EngageLab Push的多时区解决方案
    01、引言多时区问题一直是全球客户和终端用户面临的挑战之一。EngageLabPush致力于解决这个问题,确保全球各地的终端用户可以平等地享受到同样的推送服务,同时让客户能够更好地管理不同时区的应用和对应的终端用户。02、解决多时区问题的总体方案1、在服务器端,所有涉及时间的信息统......
  • 构建之法阅读笔记02
    《现代软件工程构建之法》第二章个人技术和流程,主要介绍如何通过良好的个人技术和流程,提高软件开发的效率和质量。在阅读本章后,我对自己过去在这方面的做法有了更深刻的反思和认识,同时也为自己今后的软件开发提出了更加理性和有效的解决方案。个人感受:我过去是怎样做的在个人技术......
  • 构建之法阅读笔记03
    《现代软件工程构建之法》第三章软件工程师的成长,主要介绍了软件工程师的技能、素质和职业发展规划。在阅读本章后,我对自己过去在这些方面的发展还有待提高,同时也得到了一些有益的启发和建议,可以帮助我更好地成长和发展。个人感受:我过去是怎样做的在软件开发的过程中,我过去往往注......
  • 构建之法阅读笔记01
    《现代软件工程构建之法》第一章概论介绍了软件工程的概念、软件危机及其原因,以及现代软件工程的目标、方法和原则。阅读完本章后,我深刻认识到以往自己在软件开发中存在的问题,也对如何提高软件开发的效率和质量有了更深入的思考。个人感受:我过去是怎样做的在实际的软件开发过程中,......
  • springmvc启动父子容器过程简略源码
    1启动点tomcat启动的时候会根据spi机制找到sping-web下的SpringServletContainerInitializer 2根据servlet规范SpringServletContainerInitializer会关注实现了WebApplicationInitializer的类,找到非接口非抽象的我们自定义的启动类,并调用startUp方法,如果没有实现会从父类找......
  • 【笔记】macbook m2 芯片中使用 gcc docker 镜像来交叉编译
    作者:张富春(ahfuzhang),转载时请注明作者和引用链接,谢谢!cnblogs博客zhihuGithub公众号:一本正经的瞎扯一个c程序,如何在macbookm2芯片的笔记本上,编译成linuxamd64的二进制格式呢?用gcc的docker镜像轻松的解决了这个问题:#下载gcc镜像,并且是linuxamd64......
  • Java笔记(十):函数式接口
    函数式接口有且仅有一个抽象方法的接口JDK8中,只有一个抽象方法的接口称为函数式接口,我们就能使用Lambda。针对一个接口中,是否有大于一个抽象方法?JDK8为我们新增了一个注解:@FunctionalInterface。它能够帮助我们检测这个接口是不是只有一个抽象方法,如果有两个抽象方法,则会报......
  • 源码安装mysql
    前言1.在下面网站找到需要安装的mysql版本https://downloads.mysql.com/archives/community/注意要选择的操作系统为linux-generic2.kafka安装方式链接有空可测试下:https://blog.csdn.net/m0_46192647/article/details/123424090一.mysql部署systemctlstopfirewalld&&s......
  • Java笔记(九):线程池
    三大方法Executors.newSingleThreadExecutor();//单个线程Executors.newFixedThreadPool(5);//固定的线程池大小Executors.newCachedThreadPool();//可伸缩的以上底层都是由ThreadPoolExecutor实现阿里开发手册:线程池不允许使用Executors去创建,而是通过ThreadP......
  • 考古笔记6:单臂路由子接口实现VLAN间通讯
    拓扑构成拓扑:配置过程配置信息:R1R1#conftEnterconfigurationcommands,oneperline.EndwithCNTL/Z.R1(config)#interfacef0/0R1(config-if)#noshutR1(config-if)#interfacef0/0.1R1(config-subif)#encapsulationdot1Q10//后面的数字1代表是的侦听VLAN号为10......