ThreadLocal, InheritableThreadLocal和TransmittableThreadLocal
ThreadLocal(TL)
后续部分地方会使用ThraedLocal简称为TL
什么是TL?
ThreadLocal是Java中的一个类, 也称为线程本地变量, 它提供了线程局部变量
的功能。每个ThreadLocal对象都可以存储一个线程本地的变量副本
,这意味着每个线程都可以独立地访问自己的变量副本,而不会影响其他线程的副本
为什么要使用TL?
为了更加方便大家的理解, 先从
"如果不用TL, 那么会出现什么问题"
开始, 明白问题所在, 然后引申出TL, 明白这门技术的作用, 然后才什么时候用, 什么时候不用
大家都知道, 并发
场景下, 有多个
线程同时
修改同一个共享变量
可能会导致线程安全问题, 有下述方案可以解决
- 加锁
- 通过加锁, 让带代码线性排队执行, 例如
synchronized
或Lock
- 通过加锁, 让带代码线性排队执行, 例如
- ThreadLocal
- ThreadLocal采用的是
空间换时间
- 将共享变量拷贝一份到线程的本地, 本地保存了共享变量的拷贝副本
- 多线程对共享变量修改时, 实际上修改的是变量副本, 从而保证线程安全
- ThreadLocal采用的是
TL原理
TL结构
内存结构, 先看图
Thread
类中, 有一个类型Thread.ThreadLocalMap
的实例变量, 即每个线程都有自己的ThreadLocalMap
ThreadLocalMap
内部维护了Entry
数组, 每个Entry
代表一个完整对象key
是ThreadLocal
(并不是ThreadLocal本身, 而是它的弱引用
)value
是ThreadLocal
的泛型对象值
- 线程隔离: 每个线程在往
ThreadLocal
里放值的时候,都会往自己
的ThreadLocalMap
里存,读也是以ThreadLocal
作为引用,在自己的map
里找对应的key
,从而实现了线程隔离 ThreadLocalMap
有点类似HashMap
的结构,只是HashMap
是由数组+链表实现的,而ThreadLocalMap
中并没有链表结构
// java.lang.ThreadLocal.ThreadLocalMap
static class ThreadLocalMap { // 是ThreadLocal的内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
// 与此ThreadLocal关联的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// Entry数组
private Entry[] table;
// ThreadLocalMap的构造器,ThreadLocal作为key
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
ThreadLocal的实现原理
哪些点要回答到?
- 线程和ThreadLocalMap关系?
- Entry的K-V存储的是什么?
- 并发下怎么就线程隔离了?
-
每个线程都有一个属于自己的
ThreadLocalMap
- Thread
线程类有一个类型为
ThreadLocal.ThreadLocalMap的实例变量
threadLocals
- Thread
-
Entry
的Key存ThreadLocal本身
, Value存ThreadLocal泛型值
ThreadLocalMap
内部维护着Entry
数组,每个Entry
代表一个完整的对象,key
是ThreadLocal
本身,value
是ThreadLocal
的泛型值
-
每个线程读写操作的时, 都根据
ThreadLocal
去找ThreadLocalMap
, 由于Entry中的Key存的是自己, 所以找的时候会找到自己Value进行操作- 并发多线程场景下,每个线程
Thread
,在往ThreadLocal
里设置值的时候,都是往自己的ThreadLocalMap
里存,读也是以某个ThreadLocal
作为引用,在自己的map
里找对应的key
,从而可以实现了线程隔离
- 并发多线程场景下,每个线程
TL的一些设计上问答
为什么不直接使用线程ID作为ThreadLocalMap的Key
如果在你的应用中,一个线程中只使用了一个ThreadLocal
对象,那么使用Thread
做key也未尝不可
但实际中,一个线程可能使用不止一个ThreadLocal
对象,此时存在以下问题
- 假如一个类中, 有两个ThreadLocal, 如果使用
线程ID
作为ThreadLocalMap
的Key, 通过线程ID
无法区分出要获取的是哪个ThreadLocal
- 因此, 不能使用
Thread
做key,而应该改成用ThreadLocal
对象做key,这样才能通过具体ThreadLocal对象的get
方法,轻松获取到你想要的ThreadLocal对象
代码
public class TianLuoThreadLocalTest {
private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
}
如下图
那么ThreadLocal又是怎么做到唯一区分的?
ThreadLocal
是通过threadLocalHashCode
属性唯一区分的, 每个ThreadLocal对象都有一个hash值threadLocalHashCode
,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647
public class ThreadLocal<T> {
// 初始化容量, 这个值必须是2次幂
private static final int INITIAL_CAPACITY = 16;
// hash表(桶), 容量大小必须为2次幂
private Entry[] table;
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
// 黄金分割数(斐波那契数)
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化桶, 初始容量为16
table = new Entry[INITIAL_CAPACITY];
// hash+ 取模计算桶索引
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 在该桶位置放置一个新Entry
table[i] = new Entry(firstKey, firstValue);
// size设置为1, 表示桶中有一个K-V对了
size = 1;
// 设置触发扩容的阈值为16, 后续用于判断是否需要扩容
setThreshold(INITIAL_CAPACITY);
}
}
}
HASH_INCREMENT
的值比较特殊, 被称为斐波那契数
也叫 黄金分割数
。hash
增量为 这个数字,带来的好处就是 hash
分布非常均匀
手动模拟斐波那契数
所谓的分布均匀
// 斐波那契数
int hashIncrement = 0x61c88647;
// 容量
int capacity = 16;
int hashCode = 0;
for (int i = 0; i < capacity - 1; i++) {
hashCode = i * hashIncrement;
int bucket = hashCode & (capacity - 1);
System.out.println(i + "在桶中的位置: " + bucket);
}
运行结果如下, 发现分布还是很均匀的,
在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,过程如下:
- i位置为null: 就初始化一个Entry对象放在位置i上
- 位置i已经有Entry对象: 如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value
- 位置i的Entry对象,和即将设置的key没关系,那么只能找下一个空位置;
在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置
为什么说TL可能会导致内存泄漏? 因为弱引用吗?怎么解决?
前置知识:
强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
详细介绍请戳—>Java四大引用类型
问题描述
ThreadLocal内存泄露指的是:ThreadLocal被回收了,ThreadLocalMap Entry的key没有了指向, 但Entry仍然有ThreadRef->Thread->ThreadLoalMap-> Entry value-> Object 这条引用一直存在导致内存泄露
如下图
ThreadLocalMap
使用ThreadLocal
的弱引用作为key
,当ThreadLocal
变量被手动设置为null
,即一个ThreadLocal
没有外部强引用
来引用它,当系统GC时,ThreadLocal
一定会被回收。这样的话,ThreadLocalMap
中就会出现key
为null
的Entry
,就没有办法访问这些key
为null
的Entry
的value
,如果当前线程再迟迟不结束
的话(比如线程池的核心线程),这些key
为null
的Entry
的value
就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收, 造成内存泄漏。
内存泄漏的具体条件
总结上述, 可以发现导致内存泄露的概率非常低
- 只要ThreadLocal没被回收(使用时强引用不置null),那ThreadLocalMap Entry key的指向就不会在GC时断开被回收,也没有内存泄露一说法
- ThreadLocalMap是依附在Thread上的,只要Thread销毁,那ThreadLocalMap也会销毁, 非线程池环境下,也不会有长期性的内存泄露问题
- ThreadLocal实现下还做了些"保护"措施,get/set/remove方法如果在操作ThreadLocal时,发现key为null,会将其清除掉, 线程池(线程复用)环境下如果调用了上述方法, 那么不会有长期内存泄漏的问题
也就说只要我们用完后即使手动调用remove
掉就不会出现这么多问题
模拟ThreadLocal内存泄漏
public class ThreadLocalTestDemo {
private static final ThreadLocal<MemoryClass> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());
for (int i = 0; i < 5; ++i) {
threadPoolExecutor.execute(new Runnable() {
@Override
public void run() {
MemoryClass memoryClass = new MemoryClass();
System.out.println("创建对象: " + memoryClass);
THREAD_LOCAL.set(memoryClass);
memoryClass = null; // 将对象设置为 null,表示此对象不在使用了
// THREAD_LOCAL.remove(); // 如果不手动remove, 就会出现内存泄漏, 如果使用完remove, 那么就不会内存泄漏
}
});
Thread.sleep(1000);
}
}
static class MemoryClass {
// 100M
private final byte[] bytes = new byte[100 * 1024 * 1024];
}
}
设置堆的最大值, 方便测试
发现OutOfMemoryError
异常并提示Java heap space
, 即内存泄漏了
如果使用完了就remove, 那么不会出现内存泄漏的问题, 将下述代码的注释解开, 运行
// 其它代码...
System.out.println("创建对象: " + memoryClass);
THREAD_LOCAL.set(memoryClass);
THREAD_LOCAL.remove(); // 这行代码
// 其它代码...
综上结论, 因为我们使用了线程池,线程池有很长的生命周期,因此线程池会一直持有memoryClass
对象的value值, 即使设置了memoryClass = null
, 但是对memoryClas引用还是存在的
内存图如下
i=1, 创建MemoryClass设置到ThreadLocal时
i=1, memoryClass = null时
i=2, 创建MemoryClass设置到ThreadLocal时
i=2, memoryClass = null时
i=3, 创建MemoryClass设置到ThreadLocal时
上述的Value的内存无法回收, 所以就出现了内存泄漏
源码跟踪
ThreadLocalMap
考虑到上述线程存活周期较长的情况, 导致内存泄露的问题, 在在ThreadLocal
的get
,set
,remove
方法,都会清除线程ThreadLocalMap
里所有key
为null
的value
看看源码是怎么做的
set()
往ThreadLocalMap
中set
数据(新增或者更新数据)分为好几种情况
**第一种情况:**槽位数据不为空,key
值与当前ThreadLocal
通过hash
计算获取的key
值一致
- 直接更新该槽位的数据
第二种情况: 通过hash
计算后的槽位对应的Entry
数据为空
- 直接将数据放到该槽位即可
第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entry
为null
的槽位之前,没有遇到key
过期的Entry
-
遍历散列数组,线性往后查找
- 如果找到
Entry
为null
的槽位,则将数据放入该槽位中
- 如果找到
- 如果找到的
key值相等
数据,直接更新即可
第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entry
为null
的槽位之前,遇到key
过期的Entry
大致步骤
-
初始化
探测式清理
扫描起始位置-
起始位置即
staleSlot
初始为index=6
-
-
以
staleSlot
位置向前
迭代- 以当前
staleSlot
开始 向前迭代查找,检测是否有过期的Entry
数据,如果有则更新过期数据起始扫描下标slotToExpunge
。如果找到了过期的数据,继续向前迭代, 直到碰到Entry
为null
结束
- 以当前
-
slotToExpunge
的作用用来判断当前过期槽位staleSlot
之前是否还有过期元素, 后续会使用到的
-
以
staleSlot
位置向后
迭代-
没有没有Key相同的Entry
- 迭代后没有找到Key相同的Entry
-
将
stableSlot
的Entry
的Value
置空,将传入的key-value
构造一个新的Entry
替换table[stableSlot]
位置的Entry
-
找到Key相同的Entry
- 替换旧Value
-
和staleSlot交换位置
-
/*
将指定的值与给定的ThreadLocal键相关联
*/
private void set(ThreadLocal<?> key, Object value) {
// 1. 计算索引: 将传入的key(即ThreadLocal对象)的哈希码并进行按位与操作(key.threadLocalHashCode & (len-1)),计算出在内部Entry数组tab中的目标索引位置。这个计算方式确保了哈希分布均匀且索引值始终在数组范围内
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 2. 遍历查找或替换: 从计算出的索引开始,遍历Entry数组。对于每个Entry,检查其包含的ThreadLocal对象是否与传入的key相等。如果找到匹配项,则直接将对应的Entry的值更新为传入的value,然后结束函数执行
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 2.1 如果key相等: 直接替换
if (k == key) {
e.value = value;
return;
}
// 2.2 如果key为null: 若在遍历过程中遇到Key为null的Entry, 则说明该索引位之前存放的key(threadLocal对象)被回收了,这通常是因为外部将threadLocal变量置为null,
// 又因为entry对threadLocal持有的是弱引用,一轮GC过后,对象被回收。
// 这种情况下,既然用户代码都已经将threadLocal置为null,那么也就没打算再通过该对象作为key去取到之前放入threadLocalMap的value, 因此ThreadLocalMap中会使用replaceStaleEntry替换调这个空闲Entry,
// 将新的键值对存入,并返回
if (k == null) {
// replaceStaleEntry()方法解析在后面
replaceStaleEntry(key, value, i);
return;
}
}
// 3. 创建新Key: 若遍历完对应索引位置的所有Entry仍没有找到匹配的key,说明向后迭代的过程中遇到了entry为null的情况,则在当前索引位置创建一个新的Entry(new Entry(key, value)),并将新值插入到数组中, 同时会增加size计数器
tab[i] = new Entry(key, value);
int sz = ++size;
// 4. 容量控制和扩容: 调用cleanSomeSlots()做一次启发式清理工作,清理散列数组中Entry的key过期的数据
// 如果清理工作完成后,未清理到任何数据,且size超过了阈值(数组长度的2/3),进行rehash()操作
if (!cleanSomeSlots(i, sz) && sz >= threshold) {
// rehash()中会先进行一轮探测式清理,清理过期key,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑, 扩容逻辑见rehash()分析
rehash();
}
}
总结上述
什么情况下桶可以使用?
k = key
: 说明是替换操作, 可以使用k = null
: 说明是过期桶, 执行替换逻辑, 占用过期桶Entry=null
: 表示该桶没有存放Entry,直接使用
这里提一嘴replaceStaleEntry和cleanSomeSlots的区别
replaceStaleEntry
- 触发时机: 调用新值时(即调用set方法), 发现 Entry 中的 Key 已经被垃圾回收器回收(即 Key 为 null),但是 Value 还存在时,会调用此方法进行
替换
- 目的: 将
当前线程本地变量表
中的这个无效(stale)Entry 替换为新的键值对,同时清理掉旧的、不再需要的 Value 对象引用,以防止内存泄漏- replaceStaleEntry 更侧重于在
插入新值时
立即处理遇到的无效EntrycleanSomeSlots
- 触发时机: 通用的
清理
方法, 通常在进行扩容、初始化或在某些操作后触发,用于清理整个ThreadLocalMap
中的一系列连续槽位上的无效Entry(指已被GC回收但Value尚未释放的Entry)
, 从指定的起始索引开始,按照一定的步长遍历数组的一部分,查找并移除所有 Key 为 null 的 Entry- cleanSomeSlots 则是在
一定场景
下对整个 Map 进行批量的、被动式的无效条目清理
get()
流程图解
/*
从当前执行线程的上下文中获取与之关联的线程局部变量(ThreadLocal)的值
*/
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
// 通过当前线程对象,调用getMap(t)方法获取一个ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null) {
// 查找与当前ThreadLocal实例相关Entry, 方法里面有key==null的清除逻辑
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果找到了对应Entry(即当前线程已经设置了ThreadLocal的值)
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 若在当前线程的ThreadLocalMap中没有找到对应的Entry(即当前线程尚未设置过此ThreadLocal的值),则调用setInitialValue()方法。这个方法会先调用initialValue()方法为当前线程生成一个初始值,然后将其存入当前线程的ThreadLocalMap中,并最终返回这个初始值
return setInitialValue();
}
/*
ThreadLocal的数组中查找与给定Key关联的Entry
*/
private Entry getEntry(ThreadLocal<?> key) {
// 1. 计算索引: 传入参数key的threadLocalHashCode属性与当前table长度减一的结果进行按位与操作,得到在table数组中的索引位置 i。这种哈希策略是为了将键均匀分布到数组的不同位置,减少哈希冲突
int i = key.threadLocalHashCode & (table.length - 1);
// 2. 获取指定索引Entry
Entry e = table[i];
// 3. Entry匹配: 直接返回该Entry
if (e != null && e.get() == key)
return e;
else // 如果Entry没有命中, 调用getEntryAfterMiss处理未命中情况, 里面有key==null的清除逻辑
return getEntryAfterMiss(key, i, e);
}
/*
当Entry未命中时调用
- ThreadLocal<?> key: 要查找的ThreadLocal
- int i: 根据key的hashcode计算得到的初始索引值,用于从哈希表(数组)开始搜索
- Entry e: 哈希表(数组)中初始索引值i对应的条目
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
// 1. 初始化变量
// 获取当前ThreadLocal类维护的哈希表引用
Entry[] tab = table;
// 获取哈希表的长度
int len = tab.length;
// 2. 循环遍历哈希表
// 使用while循环遍历以索引i开始的链表结构(这里通过数组模拟的链表,即开放地址法解决冲突),直到遇到空的Entry或者找到与key相等的Entry为止。
while (e != null) {
// 获取当前Entry的ThreadLocal
ThreadLocal<?> k = e.get();
// 如果找到的键k等于传入的key,说明找到了匹配的Entry,返回这个Entry
if (k == key)
return e;
// Entry的key为null,则表明没有外部引用,且被GC回收,是一个过期Entry
if (k == null)
expungeStaleEntry(i); //删除过期的Entry
else // 否则,使用nextIndex(i, len);方法计算下一个索引位置
i = nextIndex(i, len);
// 并更新当前条目e为新索引处的Entry
e = tab[i];
}
// 3. 找不到匹配的Entry
// 当循环结束仍没有找到与key相等的Entry时,表明哈希表中不存在与给定key关联的Entry,函数返回null
return null;
}
remove()
/*
从当前线程关联的ThreadLocalMap中移除与当前ThreadLocal实例相关的值
*/
public void remove() {
// 获取当前执行线程(通过Thread.currentThread()获取)内部持有的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果ThreadLocalMap不为null就调用移除
if (m != null) {
m.remove(this);
}
}
/*
从ThreadLocal对象的内部表中移除指定的键, 主要用于清理不再需要的ThreadLocal变量,防止内存泄漏
*/
private void remove(ThreadLocal<?> key) {
// 获取ThreadLocal类的内部哈希表引用
Entry[] tab = table;
int len = tab.length;
// 计算出键(key)在哈希表中的索引位置
int i = key.threadLocalHashCode & (len-1);
// 获取索引i处链表的头节点,然后不断遍历下一个节点直到链表末尾或找到目标Entry对象
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// 检查当前Entry对象的键是否与传入的key相等,如果相等
if (e.get() == key) {
// 调用e.clear()方法来清除该Entry对象内部存储的ThreadLocal引用和value,从而释放相关资源
e.clear();
// 调用expungeStaleEntry(i)方法进一步清理无效条目并重新调整哈希表状态,比如从链表中移除已经清除的Entry
// expungeStaleEntry键后续方法分析, 不再赘述
expungeStaleEntry(i);
return;
}
}
}
replaceStaleEntry()
/*
在set()的过程中, 如果发现Key=null的Entry时(已被GC回收但Value尚未释放的Entry), 则用新提供的K-V替换Entry, 并遍历清理其它Key=null的Entry
- key:要更新或插入的新Key
- value:与新Key关联的Value
- staleSlot:在搜索新key的过程中遇到的第一个Key=null的Entry的索引
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
// 获取当前ThreadLocalMap的table数组和其长度
Entry[] tab = table;
int len = tab.length;
Entry e;
// slotToExpunge表示开始探测式清理过期数据的开始下标, 默认从当前的staleSlot开始
int slotToExpunge = staleSlot;
// 以当前的staleSlot开始,向前迭代查找, for循环一直碰到Entry为null才会结束
for (int i = prevIndex(staleSlot, len); // prevIndex具体的作用见这段代码后续
(e = tab[i]) != null;
i = prevIndex(i, len)) {
// 如果找到了过期数据(即Key=null的Entry), 那么就将slotToExpunge(探测清理过期数据的开始下标)更新为这个过期数据(Key=null的Entry)所在的索引
if (e.get() == null)
slotToExpunge = i;
}
// 从staleSlot向后查, 碰到Entry为null的桶结束
for (int i = nextIndex(staleSlot, len); // nextIndex具体的作用见这段代码后续
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 获取当前遍历到的Entry的Key
ThreadLocal<?> k = e.get();
// 如果当前遍历到Entry的Key和方法传入的Key, 那么就说明这里是替换逻辑
if (k == key) {
// 将当前的Entry的Value更新为方法传入的Value
e.value = value;
// 交换当前staleSlot位置中Entry
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 如果slotToExpunge == staleSlot, 说明replaceStaleEntry()一开始向前查找过期数据时并未找到过期的Entry数据, 接着向后查找过程中也未发现过期数据
if (slotToExpunge == staleSlot) {
// 修改slotToExpunge(开始探测式清理过期数据的下标)为当前循环的index
slotToExpunge = i;
}
// 最后调用cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);进行启发式过期数据清理
// cleanSomeSlots(): 过期key相关Entry的启发式清理(Heuristically scan)
// expungeStaleEntry(): 过期key相关Entry的探测式清理
// 详细的见后续分析
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 执行到这里说明k!=key, 即当前Entry[]没有一个k和方法传入key相同
// 前驱节点扫描时未发现过期数据
// k == null说明当前遍历的Entry是一个过期数据
// slotToExpunge == staleSlot说明,一开始的向前查找数据并未找到过期的Entry
if (k == null && slotToExpunge == staleSlot) {
// 更新slotToExpunge为当前位置
slotToExpunge = i;
}
}
// 执行到这里说明往后迭代的整个过程中如果没有找到k == key的数据,且碰到Entry为null的数据, 则结束当前的迭代操作
// 将staleSlot的value置空(因为这个Entry本来就是过期的, 所以key也是过期的), 此时key和value都空,腾出了空间
tab[staleSlot].value = null;
// 将新的数据添加到table[staleSlot] 对应的slot中
tab[staleSlot] = new Entry(key, value);
// slotToExpunge != staleSlot说明完往前迭代的时候, 发现了其它的过期的Entry
if (slotToExpunge != staleSlot) {
// 开启清理数据逻辑
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
}
/*
将给定的索引值i减1,并对结果取模len,返回新的索引值。如果减1后的值小于0,则返回len-1
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
/*
将给定的索引值i加1,并对结果取模len,返回新的索引值。如果i加1后小于len,则返回i+1;否则返回0
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
cleanSomeSlots() 启发式清理
/*
了解决哈希表中可能存在过期条目的问题,它采用启发式方法对部分单元格进行扫描并移除发现的过期Entry,
参数:
i:当前不包含过期Entry的索引位置,从这个位置之后开始进行扫描。
n:控制扫描范围的参数,初始时,将扫描大约log2(n)个单元格。若在扫描过程中发现过期Entry,则将扫描范围扩大至log2(table.length)-1个额外的单元格
返回值: 是否找到并移除了过期Entry,初始值为false
*/
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
// 获取table, 和len的引用
Entry[] tab = table;
int len = tab.length;
// 循环
do {
//
i = nextIndex(i, len);
Entry e = tab[i];
// 如果当前Entry为null或者已经过期了
if (e != null && e.get() == null) {
// 更新n为table的长度,因为后续的槽位可能都与这个槽位冲突
n = len;
// 将删除标记更新为true
removed = true;
// 调用探测式扫描
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0); // n >>> 1相当于 n / 2,每次迭代都会检查哈希表的一半
return removed;
}
expungeStaleEntry() 探测式清理
遍历散列数组,从开始位置
向后
探测清理过期数据,将过期数据的Entry
设置为null
,沿途中碰到未过期的数据则将此数据rehash
后重新在table
数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null
的桶中,使rehash
后的Entry
数据距离正确的桶的位置更近一些
探测式清理触发逻辑
- 正常情况下set
- 过段时间出现过期数据
- 如果有其他数据set到map中,就会触发探测式清理操作, 同时对没过期的数据进行
rehash
, 如果rehash
计算得到index
和现在的index
不同就移动, 向后移动到Entry=null
离rehash计算index
最近的节点
探测式清理干了什么?
- 清理过期的
Entry
- 碰到正常数据,
rehash
计算该数据的位置否偏移(即计算的index和当前index是否一致),如果偏移(不一致),则重新计算slot
位置
- 桶位置理论上更接近
i= key.hashCode & (tab.len - 1)
的位置, 这样会提高整个hash表查询性能
原理图解
清空当前staleSlot
位置的数据, 往后继续探测
往后探测,发现index=7的Entry的Key不为null, 对这个keyrehash
计算后的index和现在的index相同
,不做任何处理, 继续往后探测
往后探测,发现index=8的Entry的Key不为null,将这个过期Entry
进行回收
如果index的keyrehash
计算后的位置为null,那么直接将这个元素移动到这个位置
如果index的keyrehash
后计算的位置不为null, 那么向后寻找不为null且离正确位置最近的槽位
index的key不为null,执行rehash
计算得到的index和当前位置的index一样,不做处理
当探测式清理遍历到Entry=null时候就退出循环
具体源码
/*
用于清理线程局部变量(ThreadLocal)哈希表中已过期的Entry
- staleSlot:表示哈希表中已知键为null的槽位索引,即一个已过期Entry的位置
*/
private int expungeStaleEntry(int staleSlot) {
// 1. 初始化变量
// 获取当前哈希表(Entry类型数组)
Entry[] tab = table;
// 获取哈希表的长度
int len = tab.length;
// 2. 删除指定staleSlot位置的Entry, 即将Key和Value都置为null, 同时将size-1
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 3. 循环重新哈希,直到遇到null
Entry e;
int i;
// 从staleSlot的下一个槽位开始遍历,直到遇到空槽位(即Entry=null)为止
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
// 对于每个非空槽位i,获取其中存储的ThreadLocal实例k,检查其是否为null
ThreadLocal<?> k = e.get();
// 如果k为null,则清除该槽位中Entry的Value和槽位中Entry,减少size计数器
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else { // 如果key不为null(即Entry没有没有过期)
// 重新计算当前key的下标位置h,并与当前槽位下标i比较
int h = k.threadLocalHashCode & (len - 1);
// 如果h与i不相等,说明这个Entry发生过了hash冲突,将这个Entry进行移动
if (h != i) {
// 将当前所在槽位Entry置空
tab[i] = null;
// 在正确索引h处寻找第一个空槽位(通过nextIndex方法循环查找)
// 两种情况
// 1. rehash计算的index上的Entry为nul: 直接将当前所在Entry移动过去
// 2. rehash计算的index上的Entry不为null: 往后迭代查找第一个Entry=null的槽位,将当前Entry移动到找到的空槽位
while (tab[h] != null)
h = nextIndex(h, len);
// 将条目e放置在找到的空槽位h处
tab[h] = e;
}
}
}
// 4. 返回值
// 当循环结束时,返回最后一个被检查过的空槽位的索引i,这个索引之后的所有槽位都已被检查并进行了必要的清理或调整
return i;
}
ThreadLocalMap是怎么扩容的?
桶容量和扩容阈值初始化
流程图如下
ThreadLocalMap
的构造方法是延迟加载的,也就是说,只有当线程第一次调用set()
时才set()ThreadLocalMap
createMap()
/*
触发阈值, 初始为0
*/
private int threshold;
/*
初始容量
*/
private static final int INITIAL_CAPACITY = 16;
/*
设置调整大小阈值以在最坏的情况下保持2/3的负载系数
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/*
set()方法
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else { // 首次调用set()方法的时候,会初始化并将Value设置进去
// 初始化ThreadLocalMap()
createMap(t, value);
}
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/*
初始化ThreadLocalMap
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始桶容量,INITIAL_CAPACITY=16
table = new Entry[INITIAL_CAPACITY];
// 计算key的index
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
// 将value设置到桶
table[i] = new Entry(firstKey, firstValue);
// size设置为1
size = 1;
// 设置桶的扩容阈值,即计算所得初始扩容阈值为10
setThreshold(INITIAL_CAPACITY);
}
/*
扩容因子取2/3计算扩容阈值
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
map.set()和resize()
在ThreadLocalMap.set()
方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中Entry
的数量已经达到了列表的扩容阈值(len * 2 / 3)
,就开始执行rehash()
逻辑
/*
将变量set到ThreadLocalMap中
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// map初始化好后就走这里进行set()
map.set(this, value);
} else {
createMap(t, value);
}
}
/*
set()方法调用rehash()的时机
1. cleanSomeSlots过程中没有发现过期的Entry
2. 当前size大小大于等于扩容的阈值,初始阈值len * 2/3 = 10(初始容量设置见ThreadLocalMap初始化java.lang.ThreadLocal.ThreadLocalMap#ThreadLocalMap(java.lang.ThreadLocal<?>, java.lang.Object)中的setThreshold(INITIAL_CAPACITY))
*/
private void set(ThreadLocal<?> key, Object value) {
// 其它代码...
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
/*
尝试清理过期Entry,如果清理后还是超过指定的阈值,就进行扩容
*/
private void rehash() {
// 先进行一遍探测式清理
expungeStaleEntries();
// 如果清理完后, 桶中数据大小仍然还是大于等于阈值的3/4, 那么调用resize扩容
if (size >= threshold - threshold / 4)
resize();
}
/*
探测式清理
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
// 如果当前Entry不为null且Entry未过期,那么从该位置调用一次探测式清理
if (e != null && e.get() == null) {
// expungeStaleEntry之前set()的探测式清理已经分析过,不在赘述
expungeStaleEntry(j);
}
}
}
/*
真正扩容操作
*/
private void resize() {
// 获取当前的table引用oldTab和其长度oldLen
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 计算扩容的容量, 两倍扩容
int newLen = oldLen * 2;
// 创建一个Entry数组, 容量为旧容量的两倍
Entry[] newTab = new Entry[newLen];
// count用来计算新table中数据大小
int count = 0;
// 遍历旧table, 碰到过期的Entry就移除,正常Entry就移动到新table中
for (Entry e : oldTab) {
if (e != null) {
// 获取当前指向的Entry的Key
ThreadLocal<?> k = e.get();
// 如果key=null, 表明这个Entry是过期的,将Value置空(GC回收)
if (k == null) {
e.value = null;
} else { // 如果key不为null,表示这是一个正常的Entry
// 计算这个Entry在新table中位置h
int h = k.threadLocalHashCode & (newLen - 1);
// 将Entry放置到新table中
// 1. 如果h位置没有被其它Entry占用就直接防止在这个Entry
// 2. 如果h位置有Entry了, 那么往后寻找一个空的槽位, 将这个Entry放置到新寻找到的槽位
while (newTab[h] != null) {
// 更新h位置
h = nextIndex(h, newLen);
}
newTab[h] = e;
// count+1
count++;
}
}
}
// 更新扩容阈值, setThreshold中计算扩容阈值公式: 新阈值 = newLen * 2 / 3
setThreshold(newLen);
// 更新size的大小
size = count;
// 新的table newTab 赋值给全局变量table,完成扩容过程
table = newTab;
}
resize()流程图解
准备添加
set将12添加到ThreadLocalMap
后触发rehash()
, 调用expungeStaleEntries()
进行探测扫描
探测式清理后, 发现size还是满足这个条件size >= threshold - threshold / 4 = 10 - 10 / 4 = 8
, 触发了resize()
操作, 将旧table拷贝到新table中, 完成扩容
set()的一个总流程
ThreadLocalMap Hash冲突是如何解决的?
绿色块
Entry
代表正常数据,黄色块代表Entry
的key
值为null
,已被垃圾回收。红色块表示Entry
为null
, 准备存放的位置
ThreadLocalMap
中使用了黄金分隔数来作为hash
计算因子,大大减少了Hash
冲突的概率,但是仍然会存在冲突
HashMap
解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树
而ThreadLocalMap
并没有链表结构, 也就说不能像HashMap
一样哈希冲突时在同一个槽位下挂载
结论
如果发生了Hash冲突, 那么就会线性查找, 一直找到
Entry
为null
的槽位才会停止查找,将当前元素放入此槽位中。在迭代的过程中, 比如遇到了
Entry
不为null
且key
值相等的情况,还有Entry
中的key
值为null
的情况等等都会有不同的处理, 在内存泄漏篇中的源码中已经详细阐述了
key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?
ThreadLocal
的key
既然是弱引用会不会GC贸然把key
回收掉,进而影响ThreadLocal
的正常使用?
弱引用: 具有弱引用的对象拥有更短暂的生命周期。如果一个对象只有弱引用存在了,则下次GC将会回收掉该对象(不管当前内存空间足够与否)
答案是不会的, 因为是ThreadLocal变量
引用了Key
, 所以不会被回收掉, 除非你将ThreadLocal置为null
// 创建obj对象
Object obj = new Object();
// 将obj包装成弱引用
WeakReference<Object> weakReference = new WeakReference<>(obj);
System.out.println("GC回收之前,弱引用: " + weakReference.get());
// 通知GC
System.gc();
Thread.sleep(2000);
System.out.println("GC回收之后,弱引用: " + weakReference.get());
// 手动设置为object对象为null
obj = null;
// 通知GC
System.gc();
Thread.sleep(2000);
System.out.println("对象object设置为null,GC回收之后,弱引用: " + weakReference.get());
Entry的Key为什么设置成弱引用?强引用不行?
官方文档是这样子描述的
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys
为了应对非常大且持续时间很长的使用,哈希表使用弱引用作为key
如果Key
使用强引用
- 当
ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用的话,如果没有手动删除,ThreadLocal就不会被回收,会出现Entry的内存泄漏
问题
如果Key
使用弱引用
- 当
ThreadLocal
的对象被回收了,因为ThreadLocalMap
持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value
则在下一次ThreadLocalMap
调用set,get,remove
的时候会被清除
也就说, 使用弱引用作为Entry
的Key
,可以多一层保障:弱引用ThreadLocal
不会轻易内存泄漏,对应的value
在下一次ThreadLocalMap
调用set,get,remove
的时候会被清除
实际上,我们的内存泄漏的根本原因是,不再被使用的Entry
,没有从线程的ThreadLocalMap
中删除。一般删除不再使用的Entry
有这两种方式:
- 使用完
ThreadLocal
,手动调用remove()
,把Entry从ThreadLocalMap
中删除 ThreadLocalMap
的自动清除机制去清除过期Entry
(ThreadLocalMap
的get(),set()
时都会触发对过期Entry
的清除)- 这种方式不可靠, 如果依赖ThreadLocalMap的清除机制, 就很有可能发生内存泄漏
ThreadLocal和synchronized的区别?
-
相同之处:
ThreadLocal
和synchronized
关键字都用于处理多线程并发访问变量的问题 -
不同之处
ThreadLocal
通过局部变量副本
的方式解决不同线程之间的冲突问题,采用的是空间换时间
思想synchronized
依赖JVM同步机制
,通过对象的锁机制保证同一时间只有一个线程访问变量,采用的是时间换空间
思想
Entry的Value为什么又不设计成弱引用的?
假定Value被设计成弱引用,此时Entry的Value如果被Entry引用,同时被其它业务系统的引用,如果此时某个业务将其设置成null,导致被GC回收了,可能会导致后续业务系统出现异常
使用场景和注意事项
ThreadLocal
的很重要一个注意点,就是使用完,要手动调用remove()
, 特别是在使用线程池
的时候
而ThreadLocal
的应用场景主要有以下这几种:
- 使用
日期工具类
,当用到SimpleDateFormat
,使用ThreadLocal保证线性安全 - 全局存储用户信息(用户信息存入
ThreadLocal
,那么当前线程在任何地方需要时,都可以使用) - 保证同一个线程,获取的数据库连接
Connection
是同一个,使用ThreadLocal
来解决线程安全的问题 - 使用
MDC(Mapped Diagnostic Context)
保存日志信息- 映射诊断上下文(Mapped Diagnostic Context,简称MDC)是一种工具, 理解成一个日志的扩展,
扩展的目的就是给每个线程输出的日志打上一个标记
(一个线程只有一个标记且不能重复一般使用uuid即可),这样我们在查看日志时候,就可以根据这个标记来区分调用链路
- 映射诊断上下文(Mapped Diagnostic Context,简称MDC)是一种工具, 理解成一个日志的扩展,
InheritableThreadLocal(ITL)
ThreadLocal
是线程隔离的,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的,如果我们希望父子线程共享数据, 那么就需要用到InheritableThreadLocal
(下文称为ITL
)
ThreadLocal<String> threadLocal = new ThreadLocal<>();
InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("逸一时, 误一世");
inheritableThreadLocal.set("你是一个一个一个");
Thread thread = new Thread(() -> {
System.out.println("ThreadLocal的Value: " + threadLocal.get());
System.out.println("InheritableThreadLocal的Value: " + inheritableThreadLocal.get());
});
thread.start();
运行结果如下
在子线程中,是可以获取到父线程的 InheritableThreadLocal
类型变量的值,但是不能获取到 ThreadLocal
类型变量的值
因为ThreadLocal
是线程隔离
的, 所以无法获取
InheritableThreadLocal
又为什么可以访问的到呢?
在Thread
类中,除了成员变量threadLocals
之外,还有另一个成员变量:inheritableThreadLocals
。它们两类型是一样的
public class Thread implements Runnable {
ThreadLocalMap threadLocals = null;
ThreadLocalMap inheritableThreadLocals = null;
}
Thread
类的构造方法中(源码是JDK11的)
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 其它代码...
// 获取当前线程的父线程(创建线程的当前线程就是新线程的父线程)
Thread parent = currentThread();
// 其它代码...
// 如果允许子线程继承ThreadLocal 并且父线程的inheritableThreadLocals不为空
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 将父线程的inheritableThreadLocals赋值给子线程的inheritableThreadLocals
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 其它代码...
}
InheritableThreadLocal
仍然有缺陷,一般我们做异步化
处理都是使用的线程池
,而InheritableThreadLocal
是在new Thread
中赋值的,而线程池是线程复用
的逻辑,所以这里会存在问题,可以引入阿里的TransmittableThreadLocal
解决
TransmittableThreadLocal(TTL)
InheritableThreadLocal
支持子线程访问父线程,本质上就是在创建线程的时候将父线程中的本地变量值全部复制到子线程中,而在线程池中,线程是复用的,并不用每次新建,那么此时InheritableThreadLocal
复制的父线程就变成了第一个执行任务的线程了,即后面所有新建的线程,他们所访问的本地变量都源于第一个执行任务的线程(期间也可能会遭遇到其他线程的修改),从而造成本地变量混乱
阿里的开源的TransmittableThreadLocal
(后续称为TTL
)为我们提供相关的解决方案
先演示以下使用
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
// 单例线程池
ExecutorService executorService = Executors.newSingleThreadExecutor();
itl.set("逸一时,误一世");
executorService.submit(() -> {
System.out.println("第一次从线程池中获取数据: " + itl.get());
});
itl.set("你是一个一个一个");
executorService.submit(() -> {
System.out.println("第二次从线程池中获取数据: " + itl.get());
});
运行结果如下图
分析原因
- 这里使用的是
单例线程池
,固定线程数是1 - 首次submit任务的时,线程池会初始化一个线程,创建的时候会调用构造方法初始化,此时会将父线程中
ITL
复制到子线程中, 所以第一次显示为逸一时,误一世
- 第二次submit任务的时,线程池中已经有一个线程, 直接复用,此时没有调用构造方法,所以子线程中的值没有更新,使用的还是旧数据,所以显示的还是
逸一时,误一世
如果我们想要第二次输出的是你是一个一个一个
,那么就需用阿里开源的TransmittableThreadLocal
引入对应pom文件
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>最新版本</version>
</dependency>
使用方式如下
-
方式一: JavaAgent自动修改字节码, 启动jar的时候,附加上参数-javaagent:/xx/transmittable-thread-local.jar(参数必须放首位)
-
方式二: TTL代码封装调用, 如下文所示
// 创建TTL
TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
// 使用 TtlExecutors.getTtlExecutorService() 包装一下我们自己的线程池,这样才可以 使用 TransmittableThreadLocal 解决在使用线程池等会缓存线程的组件情况下传递ThreadLocal的问题
ExecutorService executorService = TtlExecutors.getTtlExecutorService(
Executors.newSingleThreadExecutor());
ttl.set("逸一时,误一世");
executorService.submit(() -> {
System.out.println("第一次从线程池中获取数据: " + ttl.get());
});
ttl.set("你是一个一个一个");
executorService.submit(() -> {
System.out.println("第二次从线程池中获取数据: " + ttl.get());
});
运行结果如下图
原理
// com.alibaba.ttl.threadpool.TtlExecutors#getTtlExecutorService
public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {
if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {
return executorService;
}
return new ExecutorServiceTtlWrapper(executorService, true);
}
// com.alibaba.ttl.threadpool.ExecutorServiceTtlWrapper
class ExecutorServiceTtlWrapper extends ExecutorTtlWrapper implements ExecutorService, TtlEnhanced {
private final ExecutorService executorService;
// 最后我们使用的线程池也就是这个增强后的ExecutorServiceTtlWrapper了。它在这里也实现了ExecutorService接口,那么肯定是实现了里面的所有方法
ExecutorServiceTtlWrapper(@NonNull ExecutorService executorService, boolean idempotent) {
super(executorService, idempotent);
this.executorService = executorService;
}
//.....
// TTL对我们用到Runnable和Callable都进行了包装增强
@NonNull
@Override
public <T> Future<T> submit(@NonNull Callable<T> task) {
// TtlCallable.get()见com.alibaba.ttl.TtlRunnable#get(java.lang.Runnable, boolean, boolean)
// 内部其实就是多了层包装
return executorService.submit(TtlCallable.get(task, false, idempotent));
}
@NonNull
@Override
public <T> Future<T> submit(@NonNull Runnable task, T result) {
// TtlRunnable.get()见com.alibaba.ttl.TtlRunnable#get(java.lang.Runnable, boolean, boolean)
// 内部其实就是多了层包装
return executorService.submit(TtlRunnable.get(task, false, idempotent), result);
}
@NonNull
@Override
public Future<?> submit(@NonNull Runnable task) {
// TtlRunnable.get()见com.alibaba.ttl.TtlRunnable#get(java.lang.Runnable, boolean, boolean)
// 内部其实就是多了层包装
return executorService.submit(TtlRunnable.get(task, false, idempotent));
}
}
TtlRunnable
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
private final AtomicReference<Object> capturedRef;
private final Runnable runnable;
// 运行后是否 释放 Ttl 值的引用
private final boolean releaseTtlValueReferenceAfterRun;
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
// capture() 这里具体调用的是 TransmittableThreadLocal下内部类Transmitter的capture()方法
// 捕获当前线程中的所有TransmittableThreadLocal和注册的ThreadLocal值。
this.capturedRef = new AtomicReference<>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
@Override
public void run() {
// capturedRef是主线程传递下来的ThreadLocal的值
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after run!");
}
/**
* 1. backup(备份)是子线程已经存在的ThreadLocal变量
* 2. 将captured的ThreadLocal值在子线程中set进去
*/
final Object backup = replay(captured);
try {
// 执行线程的任务
runnable.run();
} finally {
// restore()方法的作用是在run()方法执行完毕后,恢复子线程的原始ThreadLocal状态。
/*
为什么需要在run执行完之后调用restore()?
1. restore里面会主动调用remove()回收,避免内存泄露(会删除子线程新增的TTL)
2. 子线程中,可能因为执行了某些操作(比如设置了新的ThreadLocal值),使得子线程的ThreadLocal状态与主线程不同, 不调用restore()的话,就会覆盖之前backup备份部分子线程的数据,这样可能在业务上有隐患
*/
restore(backup);
}
}
//....
}
replay()
/**
* 将快照重放到执行线程
* @param captured 快照
*/
public static Object replay(Object captured) {
// 重放capture()方法中捕获的TransmittableThreadLocal和手动注册的ThreadLocal中的值,本质是重新拷贝holder中的所有变量,生成新的快照
// 笔者注:重放操作一般会在子线程或者线程池中的线程的任务执行的时候调用,因此此时的holder#get()拿到的是子线程的原来就存在的本地线程变量,重放操作就是把这些子线程原有的本地线程变量备份
final Snapshot capturedSnapshot = (Snapshot) captured;
return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}
/**
* 重放TransmittableThreadLocal,并保存执行线程的原值
*/
private static WeakHashMap<TransmittableThreadLocalCode<Object>, Object> replayTtlValues(WeakHashMap<TransmittableThreadLocalCode<Object>, Object> captured) {
// 新建一个新的备份WeakHashMap,其实也是一个快照
WeakHashMap<TransmittableThreadLocalCode<Object>, Object> backup = new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>();
// 这里的循环针对的是子线程,用于获取的是子线程的所有线程本地变量
for (final Iterator<TransmittableThreadLocalCode<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocalCode<Object> threadLocal = iterator.next();
// 拷贝holder当前线程(子线程)绑定的所有TransmittableThreadLocal的K-V结构到备份中
backup.put(threadLocal, threadLocal.get());
// 清理所有的非捕获快照中的TTL变量,以防有中间过程引入的额外的TTL变量(除了父线程的本地变量)影响了任务执行后的重放操作
// 简单来说就是:移除所有子线程的不包含在父线程捕获的线程本地变量集合的中所有子线程本地变量和对应的值
/**
* 这个问题可以举个简单的例子:
* static TransmittableThreadLocal<Integer> TTL = new TransmittableThreadLocal<>();
*
* 线程池中的子线程C中原来初始化的时候,在线程C中绑定了TTL的值为10087,C线程是核心线程不会主动销毁。
*
* 父线程P在没有设置TTL值的前提下,调用了线程C去执行任务,那么在C线程的Runnable包装类中通过TTL#get()就会获取到10087,显然是不符合预期的
*
* 所以,在C线程的Runnable包装类之前之前,要从C线程的线程本地变量,移除掉不包含在父线程P中的所有线程本地变量,确保Runnable包装类执行期间只能拿到父线程中捕获到的线程本地变量
*
* 下面这个判断和移除做的就是这个工作
*/
if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 重新设置TTL的值到捕获的快照中
// 其实真实的意图是:把从父线程中捕获的所有线程本地变量重写设置到TTL中,本质上,子线程holder里面的TTL绑定的值会被刷新
setTtlValuesTo(captured);
// 回调模板方法beforeExecute
doExecuteCallback(true);
return backup;
}
private static WeakHashMap<ThreadLocal<Object>, Object> replayThreadLocalValues( WeakHashMap<ThreadLocal<Object>, Object> captured) {
final WeakHashMap<ThreadLocal<Object>, Object> backup = new WeakHashMap<ThreadLocal<Object>, Object>();
for (Map.Entry<ThreadLocal<Object>, Object> entry : captured.entrySet()) {
final ThreadLocal<Object> threadLocal = entry.getKey();
backup.put(threadLocal, threadLocal.get());
final Object value = entry.getValue();
// 如果值是标记已删除,则清除
if (value == threadLocalClearMark) threadLocal.remove();
else threadLocal.set(value);
}
return backup;
}
resotre()
/**
* 恢复备份的原快照
*/
public static void restore( Object backup) {
// 将之前保存的TTL和threadLocal原来的数据覆盖回去
final Snapshot backupSnapshot = (Snapshot) backup;
restoreTtlValues(backupSnapshot.ttl2Value);
restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}
private static void restoreTtlValues( WeakHashMap<TransmittableThreadLocalCode<Object>, Object> backup) {
// 调用执行完后回调接口
doExecuteCallback(false);
// 移除子线程新增的TTL
for (final Iterator<TransmittableThreadLocalCode<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocalCode<Object> threadLocal = iterator.next();
// 恢复快照时,清除本次传递注册进来,但是原先不存在的 TransmittableThreadLocal
// 移除掉所有不在备份里面的TTL数据,应该是为了避免内存泄漏吧
if (!backup.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
}
}
// 重置为原来的数据(就是恢复回备份前的值)
setTtlValuesTo(backup);
}
private static void setTtlValuesTo( WeakHashMap<TransmittableThreadLocalCode<Object>, Object> ttlValues) {
for (Map.Entry<TransmittableThreadLocalCode<Object>, Object> entry : ttlValues.entrySet()) {
TransmittableThreadLocalCode<Object> threadLocal = entry.getKey();
// set 的同时,也就将 TransmittableThreadLocal 注册到当前线程的注册表了
threadLocal.set(entry.getValue());
}
}
caputrue()和holder()
线程级别的的缓存,每次调用run()前后进行set和还原数据
/**
* holder - 线程级别缓存,用于保存父线程(或者明确了父线程的子线程)的TTL对象
* 1. 用WeakHashMap弱引用,为了避免内存泄漏,内存不足时弱引用自动被回收
* 2. 使用InheritableThreadLocal,作用跟ThreadLocal差不多(因为replay设置值,run(),最后还是会restore还原)
*/
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
@Override
protected WeakHashMap<TransmittableThreadLocalCode<Object>, ?> initialValue() {
// holder默认使用InheritableThreadLocal。初始化的时候会调用initialValue返回一个WeekHashMap
return new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>();
}
@Override
protected WeakHashMap<TransmittableThreadLocalCode<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocalCode<Object>, ?> parentValue) {
// 返回的是子线程在第一次get的时候的初始值,如果不重写,默认就是返回父线程的值
return new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>(parentValue);
}
};
holder拷贝
- TTL有一个静态内部类
Transmitter
,专门用于操作TTL本地线程缓存的重放、恢复备份、清除等操作。下面以TtlRunnable作为一个入口进行分析
- 通过TtlRunnable.get(runnable)进行增强调用
// 调用执行线程的时候包裹(TtlRunnable.get
executor.execute(TtlRunnable.get(runnable));
- 会调用到TtlRunnable的构造方法,然后调用到capture()拷贝方法
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
// capturedRef:拷贝副本的引用
this.capturedRef = new AtomicReference<Object>(capture());
this.runnable = runnable;
this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
// 其它代码
}
/**
* capture():拷贝副本
* - 分为TTL拷贝、ThreadLocal拷贝
*/
public static Object capture() {
// 抓取快照
return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}
/** 抓取 TransmittableThreadLocal 的快照 **/
private static WeakHashMap<TransmittableThreadLocalCode<Object>, Object> captureTtlValues() {
WeakHashMap<TransmittableThreadLocalCode<Object>, Object> ttl2Value = new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>();
// 主线程和子线程其实都是共用一个holder的,所以主线程new一个TTL并做一个set操作之后,会搞一份数据put到holder中。
// 这时候就可以进行一个副本的拷贝,遍历holder子线程的值,然后拷贝一份出来
// eg:主线程用这个ttl.set("我是主线程");,这时候holder就会对应多了要给ttl,并且值是"我是主线程"
for (TransmittableThreadLocalCode<Object> threadLocal : holder.get().keySet()) {
// threadLocal.copyValue()默认还是拷贝引用
ttl2Value.put(threadLocal, threadLocal.copyValue());
}
return ttl2Value;
}
/** 抓取 ThreadLocal 的快照 **/
private static WeakHashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
final WeakHashMap<ThreadLocal<Object>, Object> threadLocal2Value = new WeakHashMap<ThreadLocal<Object>, Object>();
// 从 threadLocalHolder 中,遍历注册的 ThreadLocal,将 ThreadLocal 和 TtlCopier 取出,将值复制到 Map 中
for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry : threadLocalHolder.entrySet()) {
final ThreadLocal<Object> threadLocal = entry.getKey();
final TtlCopier<Object> copier = entry.getValue();
// 默认拷贝的是引用
threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));
}
return threadLocal2Value;
}
总结
TTL通过增强
Runnable
,将原本位于构造方法
的变量副本的传递,推迟到线程任务执行
的时候,即在run()
中,这样即使是使用线程池的线程,也能够在使用的时候将线程的变量副本继续传递下去通过
captured/replay/restor
捕获、重放和回放机制,避免了在高并发情况下,线程池在CallerRunsPolicy
拒绝策略下,启动的异步线程和主线程在同一线程内执行,因为子线程修改线程的变量副本从而导致业务数据混乱的问题
capture
方法:抓取线程(线程A)的所有TTL
值replay
方法:在另一个线程(线程B)中,回放在capture
方法中抓取的TTL
值,并返回 回放前TTL
值的备份restore
方法:恢复线程B执行replay
方法之前的TTL
值(即备份)总结参考部分
TTL的Issues
TtlCallable
其实和TtlRunable
原理一样的
public final class TtlCallable<V> implements Callable<V>, TtlWrapper<Callable<V>>, TtlEnhanced, TtlAttachments {
private final AtomicReference<Object> capturedRef;
private final Callable<V> callable;
// 运行后是否 释放 Ttl 值的引用
private final boolean releaseTtlValueReferenceAfterCall;
private TtlCallable(@NonNull Callable<V> callable, boolean releaseTtlValueReferenceAfterCall) {
// 抓取TTL值
this.capturedRef = new AtomicReference<>(capture());
this.callable = callable;
this.releaseTtlValueReferenceAfterCall = releaseTtlValueReferenceAfterCall;
}
@Override
@SuppressFBWarnings("THROWS_METHOD_THROWS_CLAUSE_BASIC_EXCEPTION")
public V call() throws Exception {
// 获取TTL值
final Object captured = capturedRef.get();
if (captured == null || releaseTtlValueReferenceAfterCall && !capturedRef.compareAndSet(captured, null)) {
throw new IllegalStateException("TTL value reference is released after call!");
}
// 回访TTL值,并返回TTL备份
final Object backup = replay(captured);
try {
return callable.call();
} finally {
// 恢复TTL备份
restore(backup);
}
}
}
TL,ITL,TTL区别
-
ThreadLocal:单个线程生命周期强绑定,只能在某个线程的生命周期内对
ThreadLocal
进行存取,不能跨线程存取 -
InheritableThreadLocal:在子线程创建的时候,父线程会把threadLocal拷贝到子线中(但是线程池的子线程不会频繁创建,就不会传递信息)
-
TransmittableThreadLocal:解决了ITL中线程池无法传递线程本地副本的问题,在构造类似Runnable接口对象时进行初始化
参考资料
⛳面试题-简述并分析ThreadLocalMap的key为什么是弱引用
系列八、key是弱引用,gc垃圾回收时会影响ThreadLocal正常工作吗
面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)
Java的ThreadLocal,弱引用的Key使用后GC?
ThreadLocal的进化——InheritableThreadLocal
讲透 ThreadLocal 和 InheritableThreadLocal
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
还在为线程间上下文传递而烦恼,用TransmittableThreadLocal试试
ThreadLocal你懂了,你还懂TransmittableThreadLocal嘛?
阿里开源的TransmittableThreadLocal的正确使用姿势
ThreadLocal的进化——TransmittableThreadLocal
待画图
TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了❌
待重新研读