多线程之线程隔离
作者: 西魏陶渊明 博客: https://blog.springlearn.cn/ (opens new window)
西魏陶渊明
莫笑少年江湖梦,谁不少年梦江湖
[!TIP] Java多线程之隔离技术ThreadLocal源码详解
# Java多线程之隔离技术ThreadLocal源码详解
本篇文章是对ThreadLocal和InheritableThreadLocal,TransmittableThreadLocal的原理和源码进行深入分析,并举例讲解,其中前两个是JDK自带的。原理相对比较简单,其解决了单线程环境和在单线程中又创建线程(父子线程)中线程隔离的问题, TransmittableThreadLocal主要是解决,线程池中线程复用的场景。全文涉及到源码比较多阅读起来需要动脑筋思考,文章前半部分比较简单,后半部分比较困难,注意看代码注释。有不懂的可以留言。
以上是百度百科检索到的描述,相信通过上面的描述大家已经有了一个大概的了解,也相信大多数开发人员对这个类也是比较了解的,小编首先从原理开始讲解,开始吧!
# 1. ThreadLocal 的原理是什么呢 ?
其实就相当于一个Map集合,只不过这个Map 的Key是固定的,都是当前线程。 它能解决什么问题呢? 它存在的价值是什么呢?
- 它的存在就是为了线程隔离,让每个线程都能拥有属于自己的变量空间,线程之间互相不影响,为什么这么说呢? 看下代码就明白
通过上面的代码,可以发现其实ThreadLocal的set()方法就相当于
之所以能起到线程隔离的作用,是因为Key就是当前的线程,所以每个线程的值都是隔离的,就像上图那样。
其实并不是这样简单,之所以这样讲是为了,大家理解,其实这里的核心点在getMap中,从Thread中拿到一个Map,然后把value放到这个线程的map中
因为每个线程都有一个自己的Map,也就是threadLocals。从而起到了线程隔离的作用
# 2. 根据JDK原理,自己实现一个类似的
测试用例
Result:
# 3. 单线程隔离
什么是单线程隔离,这个是小编自己想的名字,其实是为了和父子线程区分开来,上面我们演示的都是属于在单一线程的情况下的使用。
# 4.父子线程隔离
什么是父子线程,需要解释下是,当我们创建一个线程,在线程内有去运行另一个线程的时候,作为子线程,如何去拿到父线程的私有属性呢?
我们怎么能拿到父线程的属性呢?
- 我们看前面标记的①,在get()时候有一个getMap(),在②有一个createMap方法
既然我们想拿到父线程的私有变量,那我们想在线程内创建线程时候,子线程能不能拿到父线程的的私有变量呢?
答案:当然是可以的,
我们看Thread的源码的时候,可以找到这样两个属性
那么它是如何实现继承的呢?我们可以在Thread的构造初始化init方法中,找到答案
看到这里我们分析,为什么ThreadLocal不能把父线程的私有变量传递给子线程?
- 因为getMap和createMap都是对threadLocals进行操作,而threadLocals变量是不能被继承的。
那么我们怎么去实现能传递呢?
其实JDK是为我们实现了一套的,这个类就是InheritableThreadLocal,我们看他为什么能实现呢? 在看代码前,我们先自己思考下,是不是InheritableThreadLocal操作的是可继承的字段inheritableThreadLocals呢?答案也是肯定的
在父线程内创建子线程的时候,子线程会在拿到父线程中的可继承的私有变量空间属性,也就是inheritableThreadLocals字段。
测试用例
# 5. 线程池线程复用隔离
在解决上面的问题后,我们来研究一个更有难度的问题,就是线程池线程复用的情况,怎么实现?
为什么会遇到这个问题呢? 是因为在线程池中核心线程用完,并不会直接被回收,而是返回到线程池中,既然是重新利用,
那么久不会重新创建线程,不会创建线程,父子之间就不会传递(如果这点没有明白,请继续看上面父子线程)。
那么这时父子线程关系的ThreadLocal值传递已经没有意义。
那么根据这个原理 ,我们继续来深入研究一波。
解决方案是什么呢?
在submit的时候把父线程copy给子线程
在execute的时候结束后吧线程的ThreadLocal清理,就能解决这个问题
2
3
上面是网上搜到的答案,小编在证实上面答案的时候走了很多坑,根本没有找到清理的代码。最后小编发现,根本就没有清理的代码,而是重新赋值的形式来实现清理。
到底是怎么来实现的呢?我们看TransmittableThreadLocal核心代码
拿到创建线程时候的备份所有线程空间 【深复制】因为浅复制会结果会被修改
在执行时候将之前的备份恢复,将最新的值返回到backup变量中
执行完成后,再将backup最新的值重新写入到TransmittableThreadLocal中
代码看起来很简洁,但是理解起来并不容易,每一步都有很多细节?我们一个一个来看
- copy方法。
TransmittableThreadLocal内维护了一个holder保存所有TransmittableThreadLocal实例当set时候addValue方法
如果还没添加就添加,null在这里只是占位,没有其他用,因为this就包含了所有值
copy方法就是将holder里面维护的TransmittableThreadLocal实例和值通过深复制的形式返回,为什么是深复制,因为引用复制可能会在其他地方值被修改。
backupAndSetToCopied方法从copide中恢复数据,然后新值返回出去,放到backup变量中
当线程已经执行完,在调用restoreBackup方法恢复backup变量中的值。
这点理解其他优点困难,尽管小编已经很努力的讲清楚,但是可以通过下面一个例子可以将以上几种方法的用处讲清。
# 请注意文中的注释!
# 问题
子线程修改变量空间值,是否会影响父线程值?
答案:当然影响。因为子线程获取父线程的inheritableThreadLocals时候,方法ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)其实是浅复制,也就是引用复制,其主要用途是从key.childValue,就是运行ThreadLocal的继承者,重写childValue方法,从而能改变父线程的本地空间ThreadLocal
交给子类去实现了
总结:
ThreadLocal 基础实现 (原理: 保存着线程中)
inheritableThreadLocals 实现了父子直接的传递 (原理: 可继承的变量空间,在Thread初始化init方法时候给子赋值)
TransmittableThreadLocal 实现线程复用 (原理: 在每次线程执行时候重新给ThreadLocal赋值)