不久前,我的同事曾经踏上坑,这恰好引起了我的兴趣。
因此,最近,我花了一些时间研究ThreadLocal的源代码。
我凝结了本质,并将其汇总到以下11个问题中。看看你怎么能站起来?
并发编程是一项非常重要的技术,可以使我们的程序更有效。
但是,在并行场景中,如果同时修改了多个线程,则可能发生线程安全问题,即变量的最终结果可能是异常的。
为了解决线程的安全问题,许多技术方法,例如:使用或访问公共资源的代码已锁定以确保代码。
但是,在一个高串联的场景中,如果多个线程同时锁定,此时将有很多锁,这可能会浪费很多时间并减慢系统的响应时间。
因此,它还提供了改变空间时间的另一个新想法:
它的核心想法是:共享变量每个都有一个,每个线程都操作自己的副本,这对另一个线程没有影响。
例如:
为了找出ThreadLocal的基本实现的原理,我们必须查看源代码。
有一个静态内部类称为:。
实际上,方法,方法和方法是最终操作是类中的数据。
班级的内部如下:
它包含一个静态内部类,该类别继承了该类,表明它是一个弱参考。
里面还有一个数组,其中:= +。
它被定义为成员变量。
以下图片了解螺纹局部的整体结构:从上图,在每个类中,都有一个成员变量,其中包含一个。
它由螺纹局部和价值组成。其中,螺纹局部对象是一个弱参考,它将在其时自动回收。值是ThreadLocal类的数据。
让我们总结与下图的参考关系:上图除外,除非进入螺纹插座对象的输入键。其他参考文献是。
需要明确说明的是我在上图中绘制了螺纹局部对象。实际上,它不一定是实际业务场景中的堆。如果将螺纹锁定定义为静态,则螺纹锁定的对象是按类塑造的对象,并且可能出现在方法区域中。
我不知道您是否考虑过这样的问题:为什么用作钥匙而不是使用钥匙?
如果仅在线程中使用一个对象,则不必使用键。
但是在实际情况下,在您的应用程序中,线程不仅可能使用螺纹插座对象。此时使用键的问题是否存在问题?
如果使用键,则在代码中定义3个螺纹插座对象。那么,您如何知道要通过线程对象获得哪个螺纹局部对象?
如下所示:
因此,不可能使用键,而是使用对象制作键,以便通过特定螺纹局部对象的方法轻松获取所需的螺纹锁定对象。
如下所示:
如前所述,在螺纹插座对象中引入了输入键,该对象使用要设计为弱参考的对象。
那你为什么要设计这个呢?
如果Key对螺纹局部对象的弱参考已更改为强引用。我们都知道螺纹局部变量对螺纹插座对象具有很强的引用。
即使螺纹局部变量生命周期结束了,它仍将其设置为空,但是由于键仍引用了threadlocal。
目前,如果使用代码,它将存在很长时间,并且不会被破坏。
这将存在:线程变量 - >线程对象 - > threadlocalmap-> entry->键 - > treenlocal对象。
然后,将不会回收螺纹局部和螺纹锁定符号,因此存在问题。
为了解决这个问题,JDK开发人员设计了条目的密钥。
清理GC时,将自动恢复对象。
如果键弱,则当螺纹局部变量点为null时,当GC清除时,将自动恢复键,并且值也将其设置为null。
如下图所示:最关键的地方在这里。
由于当前的螺纹局部变量已被指示,因此如果直接称为其或方法,它显然会出现。因为它的寿命已经结束,因此称呼它是没有意义的。
目前,如果在系统中也定义了另一个螺纹局部变量b,则三种方法的三种方法中的任何一个或任何中的任何一个都会自动触发清洁机制,以将键清除为空值。
如果键和值为null,则将入口对象通过GC回收。如果所有条目对象都回收了,则ThreadLocalMap也将被回收。
这可以在很大程度上解决问题。
特别注意的地方是:
让我们来看看弱参考的例子:
打印结果:
虚弱的构造函数是直接直接处理的对象。没有其他参考。调用GC方法后,弱参考对象将自动回收。
但是,如果以下情况是以下情况:
结果:
首先定义对象对象,然后将对象对象的引用作为弱rreference构造函数中的参数。这次调用GC后,将不会自动恢复弱参考对象。
我们条目对象中的键不是第二种情况吗?在输入构造器中,是对螺纹局部对象的引用。
如果对象强烈引用null:
结果:
在第二个GC之后,可以正常回收弱参考。
可以看出,如果同时关联强参考和弱参考,则该对象将不会被GC恢复。也就是说,在这种情况下,除非有强参考是积极断开连接。
此外,您还可能会问一个问题:为什么将条目的价值设计为弱参考?
答案:入口的价值不仅是通过条目引用的,而且可能被业务系统中的许多地方引用。如果值更改为弱参考,并且GC恢复了轻率(数据突然消失),则可能导致异常业务系统。
相比之下,进入键非常明显。
这就是为什么进入钥匙被设计为弱参考的原因,而价值并非被设计为弱参考。
通过输入对象中的密钥,它设置为弱参考,并且可以使用或方法清洁键的值为NULL的值,它是否可以完全解决内存泄漏的问题?
答案是负面的。
如下图所示:如果在ThreadLocalMap中有许多关键条目,则随后的程序未称为有效的线路或方法。
好吧,进入的价值尚未清空。
因此,将会有这样一个:线程变量 - > thread对象 - > threadlocalmap-> entry-> enter->值 - >对象。
结果是:进入和ThreadLocalMap将长时间存在,并将引起它。
前面提到的螺纹锁定仍然导致内存泄漏的问题。我们解决吗?
答:有一种调用ThreadLocal对象的方法。
它在开始时并未称为删除方法,而是在使用ThreadLocal对象之后。
首先创建一个当前的使用类别,其中包含螺纹插座的逻辑。
然后在业务代码中调用相关方法:
我们需要注意的是:请确保在代码块中调用无用的数据。如果业务代码看起来异常,您也可以及时清理无用的数据。
该方法将设置进入NULL的键和价值,以便可以及时回收GC而不会触发其他清洁机制,从而可以解决内存泄漏的问题。
如前所述,ThreadLocalMap对象的底层使用条目数组保存数据。
因此,问题是,线程局部如何定位输入阵列数据?
ThreadLocal的GET,设置,删除方法中有这样的代码行:
使用键的hashCode值,阵列的长度将减少1.键是螺纹局部对象。阵列的长度减少了1,这等同于将阵列的长度除以1,然后。
这是一种哈希算法。
让我给你一个例子:假设len = 16,key.threadlocalhashcode = 31,
因此:int i = 31&15 = 1
等效于:int i = 31%15 = 1
计算结果是相同的,但是使用效率较高。
为什么计算更有效?
答案:由于螺纹插座的初始大小是每次张开时,阵列的大小一直是2的n前部。这样做和计算,您不需要考虑高水平,因为操作的结果必须为0。仅考虑低水平和操作,因此效率更高。
如果使用哈希算法来定位特定位置,则可能发生这种情况,即在采集模具后两个不同的哈希码的值。
线程局部解决哈希冲突如何?
让我们看看它的完成方式:
再次查看该方法:
关键外观:
当哈希算法计算出的投标小于阵列的大小时,标签值是由1添加的。变为0,循环一次循环一次,出价将变为1.评估
搜索的一般过程如下图所示:如果找到最后一个,仍然找不到它,然后从头开始。我不知道您是否发现它构成了一个:。
ThreadLocal从数组中查找数据的过程大致是这样的:
从上面得知螺纹锁定的初始大小是。因此,问题是如何扩展线程局部?
该方法在该方法中被调用:
请注意,其中一种判断为:SZ(先前的尺寸+1),如果它大于或等于阈值,请调用Rehash方法。
阈值默认为0。创建ThreadLocalMap时,请致电其构造函数:
调用SetThreshold方法为阈值设置值,而此值initial_capacity为默认尺寸16。
也就是说,第一组阈值= 16 * 2/3,整流的值为:10。
换句话说,当SZ大于或等于10时,您可以考虑扩展。
Rehash代码如下:
在真正扩展容量之前,请尝试恢复钥匙的值一次,并腾出一些空间。
如果回收后的大小超过阈值的3/4以上,则需要实际扩展。
计算如下:
换句话说,添加数据后,新尺寸等于旧尺寸时需要扩展。
调整大小的每个大小都会扩大2倍。
扩展过程如下图所示:扩展的关键步骤如下:
前面引入的螺纹锁定在中间保存和获得。
但是在实际工作中,数据可以在父亲和儿子线程中共享。也就是说,该值设置为parent线程中的螺纹插座,可以在子线程中获得。
例如:
结果:
您会发现在这种情况下使用threadLocal是不兼容的。主方法是在主线程中执行的,该方法等同于父螺纹。另一个线程在主方法中打开,这等同于子线程。
显然,通过线程局限制,数据不能在父子线程中共享。
那应该怎么办?
答:使用,这是JDK随附的类,继承了ThreadLocal类。
修改代码后:
结果:
果然,在替换了sentaritableThreadLocal之后,可以在子线程中正常获得父螺纹的值。
实际上,除了线程类中的成员变量线程插入外,还有另一个成员变量:sashitablethreadLocals。
线程类的某些代码如下:
最重要的一点是,在其方法中,将父螺纹对线程的值复制到子线程。
有兴趣的朋友,您可以私下与我交谈。或者看我身后的文章,背后会有一栏。
在实际的业务场景中,通常很少使用它,并且使用了绝大多数。
那么,如何共享线程池中螺纹局部对象生成的数据?
因为它涉及不同的线程,因此直接使用ThreadLocal显然是不合适的。
我们应该使用sentariTableThreadLocal,特定代码如下:
结果:
由于此示例在一个线程池中使用,因此固定线程的数量为1。
当提交任务是第一个提交任务时,线程池将自动创建一个线程。当时使用sarthitableThreadLocal,当创建线程时,它将调用其init方法,以将父级线程中的sarthitableTheThreadLocals数据复制到子线程中。因此,我们看到数据在主线程中设置为6,并且首次从线程池中获得了正确的数据6。
之后,数据在主线程中更改为7,但是第二个线程池中的数据仍为6。
因为在第二个提交任务中,线程池中已经有一个线程,因此您将直接进行重复使用,并且您不会重新创建线程。因此,该线程的初始方法不会被调用,因此第二次我没有获得最新的数据7,并且获得了旧数据6。
所以我该怎么做?
答:使用,这不是JDK随附的类,而是阿里巴巴开源罐装包中的类。
可以通过以下POM文件介绍JAR软件包:
代码调整如下:
结果:
我们看到,使用TransMittableThreadLocal后,可以第二次正确从线程中正确获得最新数据7。
好的。
如果您仔细观察此示例,您可能会发现,除了使用类外,还可以使用方法来创建对象。
这是一个非常重要的地方。没有此步骤,线程池中的数据将无效。
创建对象,基础提交方法或对象。
以Ttlrunnable类为例,它也实现了接口及其运行方法:
该代码的主要逻辑如下:
最后,谈论使用螺纹网的使用吗?
老实说,有许多使用ThreadLocal的场景。
以下是几种常见的情况:
等等,有很多业务场景,在这里不会列出。
由于空间有限,今天的内容在这里共享。我希望您在阅读本文后会获得一些东西。
让我们提出几个问题来考虑一下:
请期待我的下一篇文章,谢谢。
如果本文对您有帮助或灵感,请帮助扫描QR码以关注它。您的支持是我坚持写作的最大动力。
寻求一个按钮和三个公司:喜欢,向前和观看。
请注意公共帐户:[Su San Said Technology],在公共帐户中答复:面试,代码文物,开发手册,具有超级粉丝福利的时间管理,除了:Jiaqun,您还可以向前者进行交流和学习许多BAT制造商,向许多Bat Manufacturersence的前辈学习
原始:https://juejin.cn/post/70977548593189901