背景分析
相信很多程序猿在平常实现功能的过程当中,都会遇到想要某些静态变量,不管是单线程亦或者是多线程在使用,都不会产生相互之间的影响,也就是这个静态变量在线程之间是读写隔离的。
有一个我们经常使用的工具类,它的并发问题就是用ThreadLocal来解决的,我相信大多数人都看过,那就是SimpleDateFormat
日期格式化的工具类的多线程问题,大家去网上搜的话,应该会有一堆人都说使用ThreadLocal。
定义
那究竟何谓ThreadLocal
呢?通过我们的Chinese English,我们也可以翻译出来,那就是线程本地的意思,而且我们是用来存放我们需要能够线程隔离的变量的,那就是线程本地变量。也就是说,当我们把变量保存在ThreadLocal当中时,就能够实现这个变量的线程隔离了。
例子
我们先来看两个例子,这里也刚好涉及到两个概念,分别是值传递和引用传递。
- 值传递
1 | 复制代码public class ThreadLocalTest { |
以上程序的输出结果是:
1 | 复制代码current thread is thread-1 num is 5 |
我们可以看到,每一个线程打印出来的都是5,哪怕我是先通过ThreadLocal.get()
方法获取变量,然后再set
进去,依然不会进行重复叠加。
这就是线程隔离。
但是对于引用传递来说,我们又需要多注意一下了,直接上例子看看。
- 引用传递
1 | 复制代码public class ThreadLocalTest { |
我们看看运行的结果
1 | 复制代码current thread is thread-0 num is 2 |
我们看到值不但没有被隔离,而且还出现了线程安全的问题。
所以我们一定要注意值传递和引用传递的区别,在这里也不讲这两个概念了。
源码分析
想要更加深入地了解ThreadLocal这个东西的作用,最后还是得回到撸源码,看看==Josh Bloch and Doug Lea==这两位大神究竟是怎么实现的?整个类加起来也不过七八百行而已。
在这里,我分开两部分来说,分别是ThreadLocal
和ThreadLocalMap
这两个的源码分析。
ThreadLocalMap源码分析
思而再三,最后还是决定先讲ThreadLocalMap
的源码解析,为什么呢?
ThreadLocalMap
是ThreadLocal
里面的一个静态内部类,但是确实一个很关键的东西,我们既然是在看源码并且想要弄懂这个东西,那我们就一定要有一种思维,那就是如果是我们要实现这么个功能,我们要怎么做?以及看到别人的代码,要学会思考别人为什么要这么做?
我希望通过我的文章,不求能够带给你什么牛逼的技术,但是至少能让你明白,我们需要学习的是这些大牛的严谨的思维逻辑。
言归正传,ThreadLocalMap
究竟是什么?我们要这么想,既然是线程本地变量,而且我们可以通过get和set方法能够获取和赋值。
1、那我们赋值的内容,究竟保存在什么结构当中?
2、它究竟是怎么做到线程隔离的?
3、当我get和set的时候,它究竟是怎么做到线程-value的对应关系进行保存的?
通过以上三个问题,再结合ThreadLocalMap
这个名字,我想大家也知道这个是什么了。
没错,它就是ThreadLocal
非常核心的内容,是维护我们线程与变量之间关系的一个类,看到是Map结尾,那我们也能够知道它实际上就是一个键值对。至于KEY是什么,我们会在源码分析当中看出来。
Entry内部类
以下源码都是抽取讲解部分的内容来展示
1 | 复制代码static class ThreadLocalMap { |
一些简单的东西直接看我上面的注释就可以了。
我们可以看到,在ThreadLocalMap这个内部类当中,又定义了一个Entry内部类,并且继承自弱引用,泛型是ThreadLocal,其中有一个构造方法,通过这个我们就大致可以猜出,ThreadLocalMap当中的key实际上就是当前ThreadLocal对象。
至于为什么要用弱引用呢?我想我源码上面的注释其实也写得很明白了,这ThreadLocal实际上就是个线程本地变量隔离作用的工具类而已,当线程走完了,肯定希望能回收这部分产生的资源,所以就用了弱引用。
我相信有人会有疑问,如果在我要用的时候,被回收了怎么办?下面的代码会一步步地让你明白,你考虑到的问题,这些大牛都已经想到并且解决了。接着往下学吧!
getEntry和getEntryAfterMiss方法
通过方法名我们就能看得出是从ThreadLocal
对应的ThreadLocalMap
当中获取Entry节点,在这我们就要思考了。
1)我们要通过什么获取对应的Entry
2)我们通过上面知道使用了弱引用,如果被GC回收了没有获取到怎么办?
3)不在通过计算得到的下标上,又要怎么办?
4)如果ThreadLocal
对应的ThreadLocalMap
不存在要怎么办?
以上这4个问题是我自己在看源码的时候能够想到的东西,有些问题的答案光看THreadLocalMap
的源码是看不出所以然的,需要结合之后的ThreadLocal源码分析。
在这我们来看看大牛的源码是怎么解决以上问题的吧。
1 | 复制代码/** |
一看这两个方法名,我们就知道这两个方法就是获取Entry节点的方法。
我们首先看getEntry(ThreadLocal<?> key)
和getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e)
这个方法就看出来了,直接根据ThreadLocal
对象来获取,所以我们可以再次证明,key就是ThreadLocal
对象,我们来看看它的流程
1、首先根据key的hashcode & table.length - 1来确定在table当中的下标
2、如果获取到直接返回,没获取到的话,就接着往后遍历看是否能获取到(因为用的是线性探测法,往后遍历有可能获取到结果)
3、进入了getEntryAfterMiss方法进行线性探测,如果获取到则直接返回;获取的key为null,则触发一次连续段清理(实际上在很多方法当中都会触发该方法,经常会进行连续段清理,这是ThreadLocal核心的清理方法)。
expungeStaleEntry方法
这可以说是ThreadLocal
非常核心的一个清理方法,为什么会需要清理呢?或许很多人想不明白,我们用List或者是Map也好,都没有说要清理里面的内容。
但是这里是对于线程来说的隔离的本地变量,并且使用的是弱引用,那便有可能在GC的时候就被回收了。
1)如果有很多Entry节点已经被回收了,但是在table数组中还留着位置,这时候不清理就会浪费资源
2)在清理节点的同时,可以将后续非空的Entry节点重新计算下标进行排放,这样子在get的时候就能快速定位资源,加快效率。
我们来看看别人源码是怎么做的吧!
1 | 复制代码/** |
上面的代码注释我相信已经是写的很清楚了,这个方法实际上就是从staleSlot开始做一个连续段的清理和rehash操作。
set方法系列
接下来我们看看set方法,自然就是要将我们的变量保存进ThreadLocal
当中,实际上就是保存到ThreadLocalMap
当中去,在这里我们一样要思考几个问题。
1)如果该ThreadLocal
对应的ThreadLocalMap
还不存在,要怎么处理?
2)如果所计算的下标,在table当中已经存在Entry节点了怎么办?
我想通过上面部分代码的讲解,对这两个问题,大家也都比较有思路了吧。
老规矩,接下来看看代码实现
1 | 复制代码/** |
以上的代码就是调用set方法往ThreadLocalMap
当中保存K-V关系的一系列代码,我就不分开再一个个讲了,这样大家看起来估计也比较方便,有连续性。
我们可以来看看一整个的set流程:
1、先通过hashcode & (len - 1)来定位该ThreadLocal
在table当中的下标
2、for循环向后遍历
1)如果获取Entry节点的key与我们需要操作的ThreadLocal
相等,则直接替换value
2)如果遍历的时候拿到了key为null的情况,则调用replaceStaleEntry
方法进行与之替换。
3、如果上述两个情况都是,则直接在计算的出来的下标当中new一个Entry阶段插入。
4、进行一次启发式地清理并且如果插入节点后的size大于扩容的阈值,则调用resize方法进行扩容。
remove方法
既然是Map形式进行存储,我们有put方法,那肯定就会有remove的时候,任何一种数据结构,肯定都得符合增删改查的。
我们直接来看看代码。
1 | 复制代码/** |
我们可以看到,remove节点的时候,也会使用线性探测的方式,当找到对应key的时候,就会调用clear将引用指向null,并且会触发一次连续段清理。
我相信通过以上对ThreadLocalMap
的源码分析,已经让大家对其有了个基本的概念认识,相信对大家理解ThreadLocal这个概念的时候,已经不是停留在知道它就是为了实现线程本地变量而已了。
那接下来我们来看看ThreadLocal
的源码分析吧。
ThreadLocal源码分析
ThreadLocal
的源码相对于来说就简单很多了,因为主要都是ThreadLocalMap这个内部类在干活,在管理我们的本地变量。
get方法系列
1 | 复制代码/** |
ThreadLocal
的get方法也不难,就几行代码,但是当它结合了ThreadLocalMap
的方法后,这整个逻辑就值得我们深入研究写这个工具的人的思维了。
我们来看看它的一个流程吧。
1、获取当前线程,根据当前线程获取对应的ThreadLocalMap
2、在ThreadLocalMap
当中获取该ThreadLocal
对象对应的Entry节点,并且返回对应的值
3、如果获取到的ThreadLocalMap
为null,则证明还没有初始化,就调用setInitialValue方法
1)在调用setInitialValue方法的时候,会双重保证,再进行获取一次ThreadLocalMap
2)如果依然为null,就最终调用ThreadLocalMap的构造方法
set方法系列
在这里我也不对ThreadLocal
的set方法做太多介绍了,结合上面的ThreadLocalMap
的set方法,我想就可以对上面每个方法思考出的问题有个大概的答案。
1 | 复制代码public void set(T value) { |
其实ThreadLocal
的set方法很简单的,最主要的都是调用了ThreadLocalMap
的set方法,里面才是真正核心的执行流程。
不过我们照样来看看这个流程:
1、获取当前线程,根据当前线程获取对应的ThreadLocalMap
2、如果对应的ThreadLocalMap
不为null,则调用其的set方法保存对应关系
3、如果map为null,就最终调用ThreadLocalMap
的构造方法创建一个ThreadLocalMap
并保存对应关系
执行流程总结
源码分析总结
上面通过对ThreadLocal
和ThreadLocalMap
两个类的源码进行了分析,我想对于ThreadLocal这个功能的一整个流程,大家都有了个比较清楚的了解了。我真的是很佩服==Josh Bloch and Doug Lea==这两位大神,他们在实现这个东西的时候,不是说光实现了就可以了,考虑了很多情况,例如:GC问题、如何维护好数据存储的问题以及线程与本地变量之间应该以何种方式建立对应关系。
他们写的代码逻辑非常之严谨,看到这区区几百行的代码,才真正地发现,我们其实主要不是在技术上与别人的差距,而是在功能实现的一整套思维逻辑上面就与他们有着巨大的差距,最明显的一点就是,我们单纯是为了实现而实现,基本上不会考虑其他异常情况,更加不会考虑到一些GC问题。
所以通过该篇源码的分析,让我真正地意识到,我们不能光是看源码做翻译而已,我们一定要学会他们是如何思考实现这么个功能,我们要学会他们思考每一个功能的逻辑。
本文转载自: 掘金