创意电子

标题: 吃透源码的每一个细节和设计原理--ThreadLocal [打印本页]

作者: Java技术架构    时间: 2019-10-22 18:19
标题: 吃透源码的每一个细节和设计原理--ThreadLocal
专注于Java范畴优质技术,欢迎关注
作者:口试君
引言

ThreadLocal 是口试过程中非常高频的一个类,这类的复杂程度绝对是可以带出一系列连环炮的口试轰炸。biu biu biu ~~~~.
一直以为自己对这个类很相识了,但是直到去看源码,接二连三的技术浮出水面(弱引用,制止内存溢出的操作,开放地址法办理hash 冲突,各种内部类的复杂的关系),看到你怀疑人生,直到根据代码一步一步的画图才最终理解(所以本篇文章会有大量的图)。 这里也给大家一个启示,面对复杂的事情的时候,实在被问题绕晕了,就画图吧,借助图可以让问题可视化,便于理解。
WAHT

ThreadLocal 是一个线程的本地变量,也就意味着这个变量是线程独有的,是不能与其他线程共享的,这样就可以制止资源竞争带来的多线程的问题,这种办理多线程的安全问题和lock(这里的lock 指通过synchronized 大概Lock 等实现的锁) 是有本质的区别的:
当然他们的利用场景也是不同的,关键看你的资源是需要多线程之间共享的照旧单线程内部共享的
利用

ThreadLocal 的利用是非常简单的,看下面的代码

                               
登录/注册后可看大图

看到这里是不是以为特殊简单?别高兴太早,点进去代码看看,你绝对会怀疑人生
源码分析

在分析源码之前先画一下ThreadLocal ,ThreadLocalMap 和Thread 的关系,如果你对他们的关系还不相识的话,请看我的另一篇文章BAT口试必考:ThreadLocal ,ThreadLocalMap 和Thread 的关系

                               
登录/注册后可看大图

set 方法

                               
登录/注册后可看大图

createMap 方法只是在第一次设置值的时候创建一个ThreadLocalMap 赋值给Thread 对象的threadLocals 属性进行绑定,以后就可以直接通过这个属性获取到值了。从这里可以看出,为什么说ThreadLocal 是线程本地变量来的了

                               
登录/注册后可看大图

值真正是放在ThreadLocalMap 中存取的,ThreadLocalMap 内部类有一个Entry 类,key是ThreadLocal 对象,value 就是你要存放的值,上面的代码value 存放的就是hello word。ThreadLocalMap 和HashMap的功能雷同,但是实现上却有很大的不同:
为什么ThreadLocalMap 接纳开放地址法来办理哈希冲突?
jdk 中大多数的类都是接纳了链地址法来办理hash 冲突,为什么ThreadLocalMap 接纳开放地址法来办理哈希冲突呢?首先我们来看看这两种不同的方式
链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:

                               
登录/注册后可看大图

开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表充足大,空的散列地址总能找到,并将记录存入。
好比说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):

                               
登录/注册后可看大图

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:

                               
登录/注册后可看大图

链地址法和开放地址法的优缺点
开放地址法:
链地址法:
ThreadLocalMap 接纳开放地址法原因
弱引用

如果对弱引用不相识的同学,先看下我之前的写的一篇文章别再找了,一文彻底解析Java 中的弱引用(参考官网)系
接下来我们看看ThreadLocalMap 中的存放数据的内部类Entry 的实现源码

                               
登录/注册后可看大图

我们可以知道Entry 的key 是一个弱引用,也就意味这可能会被垃圾回收器回收掉
threadLocal.get()==null也就意味着被回收掉了
ThreadLocalMap set 方法

                               
登录/注册后可看大图


                               
登录/注册后可看大图

照旧拿上面解释开放地址法解释的例子来说明下。 好比说,我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) = key mod l0。 当计算前S个数{12,33,4,5,15,25}时,并且此时key=33,k=5 已经逾期了(蓝色代表为空的,可以存放数据,赤色代表key 逾期,逾期的key为null):

                               
登录/注册后可看大图


                               
登录/注册后可看大图

replaceStaleEntry 这个方法

                               
登录/注册后可看大图


                               
登录/注册后可看大图


                               
登录/注册后可看大图

第一个for 循环是向前遍历数据的,直到遍历到空的entry 就制止(这个是根据开放地址的线性探测法),这里的例子就是遍历到index=1就制止了。向前遍历的过程同时会找出逾期的key,这个时候找到的是下标index=3 的为逾期,进入到
if (e.get() == null) slotToExpunge = i;注意此时slotToExpunge=3,staleSlot=5
第二个for 循环是从index=staleSlot开始,向后编列的,找出是否有和当前匹配的key,有的话进行清算逾期的对象和重新设置当前的值。这个例子遍历到index=6 的时候,匹配到key=15的值,进入如下代码

                               
登录/注册后可看大图

先辈行数据互换,注意此时slotToExpunge=3,staleSlot=5,i=6。这里就是把5 和6 的位置的元素进行互换,并且设置新的value=new,互换后的图是这样的

                               
登录/注册后可看大图

为什么要互换
这里解释下为什么互换,我们先来看看如果不互换的话,经过设置值和清算逾期对象,会是以下这张图

                               
登录/注册后可看大图

这个时候如果我们再一次设置一个key=15,value=new2 的值,通过f(15)=5,这个时候由于上次index=5是逾期对象,被清空了,所以可以存在数据,那么就直接存放在这里了

                               
登录/注册后可看大图

你看,这样整个数组就存在两个key=15 的数据了,这样是不允许的,所以一定要互换数据expungeStaleEntry

                               
登录/注册后可看大图


                               
登录/注册后可看大图

接下来我们详细模拟下整个过程 根据我们的例子,key=5,15,25 都是冲突的,并且k=5的值已经逾期,经过replaceStaleEntry 方法,在进入expungeStaleEntry 方法之前,数据结构是这样的

                               
登录/注册后可看大图

此时传进来的参数staleSlot=6,

                               
登录/注册后可看大图

这个时候会把index=6设置为null,数据结构变成下面的情况

                               
登录/注册后可看大图

接下来我们会遍历到i=7,经过int h = k.threadLocalHashCode & (len - 1) (现实上对应我们的举例的函数int h= f(25)); 得到的h=5,而25现实存放在index=7 的位置上,这个时候我们需要从h=5的位置上重新开始编列,直到遇到空的entry 为止

                               
登录/注册后可看大图

这个时候h=6,并把k=25 的值移到index=6 的位置上,同时设置index=7 为空,如下图

                               
登录/注册后可看大图

其实目标跟replaceStaleEntry 互换位置的原理是一样的,为了防止由于回收掉中间那个冲突的值,导致背面冲突的值没办法找到(因为e==null 就跳出循环了)
ThreadLocal 内存溢出问题

通过上面的分析,我们知道expungeStaleEntry() 方法是帮助垃圾回收的,根据源码,我们可以发现 get 和set 方法都可能触发清算方法expungeStaleEntry(),所以正常情况下是不会有内存溢出的 但是如果我们没有调用get 和set 的时候就会可能面临着内存溢出,养成好习惯不再利用的时候调用remove(),加快垃圾回收,制止内存溢出
退一步说,就算我们没有调用get 和set 和remove 方法,线程结束的时候,也就没有强引用再指向ThreadLocal 中的ThreadLocalMap了,这样ThreadLocalMap 和内里的元素也会被回收掉,但是有一种伤害是,如果线程是线程池的, 在线程实行完代码的时候并没有结束,只是归还给线程池,这个时候ThreadLocalMap 和内里的元素是不会回收掉的
来源:掘金 链接:https://juejin.im/post/5d8b2bde51882509372faa7c

作者: 唐英俊640    时间: 2019-10-22 20:20
转发了
作者: 波澜梦    时间: 2019-10-23 07:41
转发
作者: Beyondandbeyond    时间: 2019-10-23 12:44
转发了
作者: 松冈孤月    时间: 2019-10-24 22:07
源码
作者: 铜豌豆的生活    时间: 2019-10-24 23:19
转发了
作者: 我是董郎啊    时间: 2019-10-25 09:06
转发了
作者: 逞不完的強    时间: 2019-10-30 08:57
聊聊threadlocal
作者: badBoy01    时间: 2019-12-30 22:37
弱引用那说的不太对吧
作者: 崔斯特顿    时间: 2020-1-4 23:44
转发了
作者: coding的达叔    时间: 2020-1-5 02:41
转发了
作者: Kumas    时间: 2020-2-9 08:57
转发了
作者: mark这时光    时间: 2020-2-16 19:00
手动赞一个
作者: mark这时光    时间: 2020-2-16 19:05
转发
作者: 张多坤Andrew    时间: 2020-2-16 20:07
基础的东西还是需要翻看
作者: Michae1    时间: 2020-4-7 07:44
吃透源码的每一个细节和设计原理--ThreadLocal
作者: 鱼和水和烧烤摊    时间: 2020-4-17 11:06
再告诉你一个,你的用法是错的,ThreadLocal是用作静态变量的,不是局部变量
作者: Guccang1    时间: 2020-4-26 10:26
开放地址法 很占用内存,还是拉链大法好。再hash也可以。
作者: jinlinlucky    时间: 2020-11-4 06:54
吃透源码的每一个细节和设计原理--ThreadLocal
作者: 西新桥大猛男    时间: 2020-11-22 12:33
学习了
作者: MovW    时间: 2021-2-19 23:54
写的很好




欢迎光临 创意电子 (https://wxcydz.cc/) Powered by Discuz! X3.4