37_源码分析:ThreadLocal如何实现线程本地化存储的?

37_源码分析:ThreadLocal如何实现线程本地化存储的?

上篇文章我们提出了线程不安全的问题,产生这个问题的原因在于变量的共享,为了解决线程不安全的问题,我们想到了让每个线程都拥有自己的对象副本,也就不存在多线程变量的共享问题,我们再看图1,回忆一下
在这里插入图片描述

图1
从图1中,我们知道,每个线程都拥有自己的对象副本,但是,如果我们每次想获取每个线程的私有对象的时候,都通过一堆复杂的代码逻辑去获取这个对象就有点麻烦了,从而我们想到了,用一个代理类把获取线程所持有的对象细节隐藏掉,请看下图2
在这里插入图片描述

图2
上篇文章结尾我们留下了一个问题,JDK是否有类似ObjectProxy解决方式呢?答案是有。如果有,JDK是怎么实现的呢?如果你来设计类似ObjectProxy,你会怎么考虑呢?那我们就带着上面的问题开始今天的学习。
在java里提供了可支持线程本地化存储的ThreadLocal类,后面我们会讲解ThreadLocal的实现,这里先来聊聊,如果让我们自己设计线程本地化存储,该如何设计呢?
其实对于线程本地化存储,我们的目标就是让不同的线程拥有不同变量,那我们第一个能想到的就是Key-Value键值对的存储,Key表示的就是线程,Value对应的就是每个线程所拥有的变量,我们来看示意图图3和代码示例图4
在这里插入图片描述

图3
在这里插入图片描述

图4
从上述图3和图4中,我们能清晰的看到,我们自己所设计的线程本地化存储,核心数据结构就是一个Map,key对应的就是线程,value对应的线程持有的对象,我们可以调用对应get和set方法可以获取和设置线程所拥有的对象,哇哦,看起来很完美,那这么设计是否有问题呢?是的,这样设计容易发生内存泄露,我们来看图5
在这里插入图片描述

图5
从上述图5中,我们能清晰的看到,我们自己实现的线程本地化存储MyThreadLocal里的map是持有线程Thread对象的,只要MyThreadLocal对象存活在jvm中,那么map中的线程Thread对象是不会被jvm垃圾回收的,所以容易出现内存泄露。那有没有别的方法避免这种情况呢?那我们就一起来看看JDK的ThreadLocal是怎么实现的吧
在这里插入图片描述

图6
从图6中,我们能看到jdk中的ThreadLocal所拥有的的对象属性,奇怪的发现,好像这些变量都没有能存储线程本地化对象的地方,同学们,先别急,如果ThreadLocal实现了线程本地化的功能,就一定会提供给我们获取和设置本地化对象的方法,那我们这接下来看看ThreadLocal有没有类似get和set方法呢?
在这里插入图片描述

图7
从图7中,我们很高兴的看到了ThreadLocal的get方法,那同学们我们倒过来想想,get方法一定是从底层存储层获取数据的,那我们是不是可以顺着get方法的逻辑,就能找到ThreadLocal的底层存储呢?按照这个逻辑,请看下图8

在这里插入图片描述

图8
在图8中,红线1,我们能看到有个叫ThreadLocalMap的东西,然后在红线2中,再从获取到的ThreadLocalMap中获取Entry,最后从取到的Entry中获取线程的value,好,那我们就按照这个逻辑一步一步来看看这些都是什么妖魔鬼怪。
在这里插入图片描述

在这里插入图片描述

图9
从图9中,我们能清晰的看到ThreadLocalMap是Thread对象里的属性,换句话说,每个Thread对象都拥有一个ThreadLocalMap对象属性,到了这里,同学们可能有点蒙了,我们知道线程本地化对象就是存储在这个ThreadLocalMap里,但这ThreadLocalMap不是ThreadLocal持有的属性,和我们自己想象的不太一样,我们通过一张图来描述一下,请看图10
在这里插入图片描述

图10
到此为止,同学们应该感觉到了,我们自己设计的方案和JDK的实现是不一样的,我们设计的里的Map是ThreadLocal所持有的,但是JDK里,这个ThreadLocalMap是Thread自己的属性,显然这个JDK的设计更为合理一点,因为ThreadLocal只是一个我们所说的ObjectProxy代理工具类,内部不应该持有任何与线程有关的属性,所以我们设置的线程本地化对象,在自己Thread类里所持有也更为容易理解些,也就是说ThreadLocalMap由Thread持有更为合理些。
到了这里,同学们,还记得图8吗?我们再来回忆一下图8,如果我们想获取线程对应的value,首先就要先获取线程对应的ThreadLocalMap,然后再从ThreadLocalMap里获取Entry,最后从Entry获取对应的value,那么我们接下来就看看ThreadLocalMap里有什么,请看图11

图11
从图10中,我们能看到,原来ThreadLocalMap是ThreadLocal一个内部类,在红线2处每个ThreadLocalMap持有一个Entry数组,且在红线1处每个Entry持有一个value对象,这个时候,同学们可能已经豁然开朗了,这个value不就是我们想找的嘛,原来整个获取value的顺序就是:线程Thread->ThreadLocalMap->Entry->value,但是我们在红线2处发现这个Entry的数组,同学们有没有这个疑问?我们是怎么样从Entry的数组中获取某一个的Entry的呢?请看图12

图12
从图8中红线2处,在获取到ThreadLocalMap之后,会执行TheadLocalMap的getEntry方法,方法如图12所示,在图12红线1处,我们清晰看到方法的参数是Threadlocal类型,红线2处,再根据传入的这个ThreadLocal的threadLocalHashCode计算坐标值,然后根据坐标值再从Entry数据里获取对应的Entry对象,从而获取到Entry里的value值,老规矩,我们再用一张图来描述一下,请看下图13

图13
从图13中,我们能清晰看到,整个线程Thread本地化存储结构,每个线程Thead里的ThreadLocalMap里可以存储多个ThreadLocal本地化对象,且每一个ThreadLocal本地化对象是通过自己的threadLocalHashCode来计算数组下标,分配到下标对应Entry数组中,从而可以进行本地化对象获取和设置操作,但是同学们这时候会说,我们自己设计的方案容易产生内存泄露,那JDK实现就没有这个情况吗?那我们就来看看JDK是如何巧妙设计的
请看下图14

图14
在图14中,我们能看到Entry的定义,想必同学们看到了Entry继承了WeakReference类,他就是弱引用,这说明了ThreadLocalMap里的对Threadlocal的引用是弱引用,请看下图15

图15
同学们,只要Thread对象被垃圾回收,那么ThreadLocalMap就能被回收,所以就不会出现内存泄漏的情况,虽然JDK线程本地化的实现的复杂了一些,但是它更加安全,所以说JDK的方案还是很优秀的,最后,我们来对比一下我们自己设计的方案和JDK的方案,请看下图16

图16
最后,通过图16对比来看,我相信同学们都有这种感觉,就是没有对比就没有伤害,从数据亲缘性来看,JDK把线程本地化存储放在Thread的ThreadLocalMap持有,且可以设置多个ThreadLocal,而我们的设计是把线程本地化存储放在自己设计的ObjectProxy代理工具类里,且容易产生泄露,而JDK通过精巧的设计却避免了内存泄露的情况,希望同学们已经get到了JDK的精良设计。
后面的文章,我们会有实战案例环节,敬请期待!


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值