- 联系方式:1761430646@qq.com
- 编写时间:2022年8月6日15:42:21
- 菜狗摸索,有误勿喷,烦请联系
1. ThreadLocal 引出
-
现在假设一个场景,在我们的程序内部,某个线程的执行链如下
-
假设如果在方法B执行时,我们得到了数据X,而这个数据X需要在方法E中用到
-
解决方法:
-
通过方法参数依次传递
- 也即将
数据X
当做方法C
的方法参数,这样方法C
就可得到数据X
- 再将
数据X
当做方法D
的方法参数,这样方法D
就可得到数据X
- 依次类推
- 最终
方法E
可获得数据X
- 缺点:
- 某个方法的方法参数应只包含本部分逻辑处理需求值,很明显,通过方法参数这种形式传递,对于
方法C
,方法D
来说,造成不必要的方法参数传递 - 如果这条执行链很长,也就
方法B
和方法E
间隔很远,那么就会显得很杂乱,逻辑模糊,同时方法参数不好管理,比如说某两个方法之间又存在数据共享,那么这两个方法之间的所有方法又得加上这个共享数据作为方法参数–人麻了
- 某个方法的方法参数应只包含本部分逻辑处理需求值,很明显,通过方法参数这种形式传递,对于
- 也即将
-
通过一个专门用来存储这条执行链共享数据的工具类来解决
- 缺点:
- 一个存储工具类对应一条执行链,但实际开发中很有可能是超级多执行链,如果构建多个存储工具类的话,在实现、管理方法就很繁琐–程序猿最喜欢的就是偷懒了
- 还有个超级致命问题就是,对于同一条执行链,在并发场景下,会有多个线程对同一数据进行读写,会有线程安全问题
- 缺点:
-
也就是我们本期的主人公:
ThreadLocal
-
这里其实也就借用了上述 2 的存储介质的思想
-
在这里先要知道在
Java
中,Thread 代表的是线程,也就我们可以通过Thread
来进行线程的操作 -
对于上述
- 一个线程对应有它自己的执行链
- 在执行链内需要借助存储介质来达到数据共享
- 同一条执行链会有多个线程执行,要防止对共享数据进行并发修改问题
-
可知,这里很多问题都牵扯到了线程
-
那么我们试想一下,是否可以使用线程
Thread
来作为存储介质? -
如果采用了线程
Thread
来作为存储介质-
首先,这里解决了共享数据的存储问题
-
其次,对于某个线程来说,它有自己的执行链,对于另外的线程,不管其执行链不同还是相同,操作时我操作的都是当前线程存储的数据,不管其他线程屁事
-
这样即保证了共享数据传输,又保证了共享数据的线程安全。(数据存放在其所在线程
Thread
中(可以理解为这个数据是线程私有的),不同线程之间互不影响)
-
-
-
2. ThreadLocal 是什么
- 官方点说法:即线程本地量,它的核心思想是共享变量在每个线程都有一个副本,多线程操作这个变量的时候,实际上都是在操作当前线程的副本,对另外的线程没有影响,从而起到了线程隔离的作用,避免了并发场景下的线程安全问题。
- 个人理解:其实就是一个工具类,解决了线程内的数据共享问题,省去了我们将共享数据存入/取出
Thread
时的一些复杂操作
3 ThreadLocalMap
-
在
ThreadLocal
类中有一个静态内部类ThreadLocalMap
,相当于变种的HashMap
-
而在
ThreadLocalMap
内部的静态内部类Entry
则是封装的键值对,其k
为ThreadLocal
类型,v
为我们存储的数据类型 -
很重要的一点是其
k
是弱引用,后面会有篇幅说这个问题 -
在
ThreadLocalMap
内部是采用链表冲突法解决hash
冲突的 -
其结构如下图:
-
作用:作为线程
Thread
中存储介质的类型
4. ThreadLoca 常用方法
4.1 get()
-
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的存储载体 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) { // map 已经初始化则根据当前 ThreadLocal 对象作为方法参数,也即是为 K,获取其封装的键值对 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { // 其键值对不为 null,直接返回对应的 V @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 当前线程的存储载体未实例化/不存在当前ThreadLocal对象作为 K 的键值对,走初始化逻辑 return setInitialValue(); }
4.2 initialValue()
-
protected T initialValue() { return null; }
-
作用:设置每个键值对的
v
默认值都为null
4.3 setInitialValue()
-
private T setInitialValue() { // 设置键值对的`v`默认为 null T value = initialValue(); // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的存储载体 ThreadLocalMap ThreadLocalMap map = getMap(t); if (map != null) // map 已经初始化,则直接设置插入这个键值对 map.set(this, value); else // map 还未实例化,走创建逻辑 createMap(t, value); // 返回默认的`v`,即为`null` return value; }
-
作用:走初始化流程
- 当前线程的存储载体还没初始化的话走创建逻辑,并把当前
ThreadLocal
对象作为k
,null
作为v
,存放到ThreadLocalMap
中去; - 如果当前线程的存储载体已经初始化的,还是把把当前
ThreadLocal
对象作为k
,null
作为v
,存放到ThreadLocalMap
中去
- 当前线程的存储载体还没初始化的话走创建逻辑,并把当前
4.4 createMap(Thread t, T firstValue)
-
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
-
作用:为当前线程实例化其存储载体
ThreadLocalMap
,并把当前ThreadLocal
对象作为k
,null
作为v
,存入到ThreadLocalMap
中去
4.5 nextIndex(int i, int len)
-
/** * i : 为当前索引位置 * len:为数组的长度 */ private static int nextIndex(int i, int len) { // 返回在当前数组中索引 i 的下一个位置,如果达到数组末尾,则重头来 return ((i + 1 < len) ? i + 1 : 0); }
4.6 remove()
-
public void remove() { // 获取当前线程的存储载体`ThreadLocalMap` ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) // 在 map 中删除以当前`ThreadLocal`对象作为`k`的键值对 m.remove(this); }
4.7 remove(ThreadLocal<?> key)
-
private void remove(ThreadLocal<?> key) { // 获取其数组 Entry[] tab = table; // 获取数组长度 int len = tab.length; // 计算当前`ThreadLocal`作为`k`时,其键值对在数组中的位置 int i = key.threadLocalHashCode & (len-1); // 遍历数组,找到对应键值对--由于采用链表冲突法,所以一开始计算得到的索引位置由于hash冲突的原由,可能不是当前`ThreadLocal`对象作为`k`对应的键值对的实际存储位置 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
4.8 getMap(Thread t)
-
ThreadLocalMap getMap(Thread t) { // 获取当前线程的存储载体`ThreadLocalMap` return t.threadLocals; }
4.9 set(T value)
-
public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的存储载体`ThreadLocalMap` ThreadLocalMap map = getMap(t); if (map != null) // map 以实例化则直接存入数据 map.set(this, value); else // map 还为实例化则先进行实例化--实例化时会把当前数据第一次存入 createMap(t, value); }
5. ThreadLocal 应用场景
- 在
Spring
事务中,保证一个线程下,一个事务的多个对数据的操作拿到的是同一个Connection
- 获取当前登录用户上下文
6. 内存泄漏问题–瞎搞,不严谨
-
先来看看其结构图
-
很明显的一点是设计者们设计成将
Key
弱引用指向ThreadLocal
对象 -
为什么设计成弱引用,不妨我们现在假设为强引用
-
在代码中,如果我们设置
ThreadLocal
变量为null
,即想销毁这个对象时 -
在
JVM
发生GC
时,虽然说当前ThreadLocal
变量已经设置为null
,不再引用这ThreadLocal
对象 -
但是
Key
还是在强引用着ThreadLocal
对象 -
所以,
GC
时是根本不会回收这个ThreadLocal
对象 -
也就是一致存在着这样一条引用链:
Thread变量
–>Thread对象
–>ThreadLocalMap
–>Entry
–>Key
–>ThreadLocal
对象 -
导致
ThreadLocal
对象一直回收不了,也就是所谓的内存泄漏
-
而当设计成弱引用时,按照上述逻辑走,
ThreadLocal
对象就能被正常回收 -
这时候在
ThreadLocalMap
中就存在了其key
为null
的Entry
对象 -
还存在着一条强引用链:
Thread变量
–>Thread对象
–>ThreadLocalMap
–>Entry
–>Value
-
试想一下,当我们在代码中手动设置
ThreadLocal
对象为null
时,我们希望在GC
时不仅此对象被回收,还希望其对应的Entry
被回收(key
都为null
了,这时候这个Entry
对象存在的意义的不大) -
但是现在看起来好像不行,并且
Value
一直被Entry
对象强引用着,想回收Value
都回收不了,还是有内存泄漏问题 -
而设计者们早已想到这点,所以它们设计成当下一次调用
ThreadLocalMap
的set()
、get()
、方法时,就会手动去除这些key
为null
的Entry
对象 -
这样
Value
少了被Entry
对象强引用这一层,你想回收Value
随时能回收,比如说直接设置为null
-
现在看起来好像解决了这个内存泄漏问题
-
但是,但是,有没有想过,在这个线程的后续声明周期中,我都没有再去调用对象的那几个方法
-
那么这个没啥意思的
Entry
对象还存在着以及Value
还是一致被强引用着,想回收都回收不了,还是有内存泄漏问题 -
嗯哼?这时候咋搞
-
在业务代码中手动调用
ThreadLocalMap
对象的remove()
方法,自己去控制–这时候就考研程序员的功力了 -
remove()
方法中会把Entry
中的Key
和Value
都设置成null
,这样就能被GC
及时回收,无需触发额外的清理机制
7. 父子线程共享数据
-
使用
InheritableThreadLocal
,它是JDK
自带的类,继承了ThreadLocal
类 -
使用:
-
实现原理:
-
Thread 类除了含有存储当前线程共享数据载体的
threadLocals
-
还有存储父子线程共享数据载体的
inheritableThreadLocals
-
很重要的一点:在线程的
init
方法中,当判断父线程的inheritableThreadLocals
不为空时,会把父线程的inheritableThreadLocals
复制给子线程
-
8. 总结
- 人麻了,不知所云
- 现在所处水平的理解基本到这了,特别感觉内存泄漏逻辑有问题,以后再继续深入
- 至少
ThreadLocalMap
的数组大小设计16,也就是2的n次幂,HashMap
中有叙述到