ThreadLocal, InheritableThreadLocal和TransmittableThreadLocal

ThreadLocal, InheritableThreadLocal和TransmittableThreadLocal

ThreadLocal(TL)

后续部分地方会使用ThraedLocal简称为TL

什么是TL?

ThreadLocal是Java中的一个类, 也称为线程本地变量, 它提供了线程局部变量的功能。每个ThreadLocal对象都可以存储一个线程本地的变量副本,这意味着每个线程都可以独立地访问自己的变量副本,而不会影响其他线程的副本

为什么要使用TL?

为了更加方便大家的理解, 先从"如果不用TL, 那么会出现什么问题"开始, 明白问题所在, 然后引申出TL, 明白这门技术的作用, 然后才什么时候用, 什么时候不用

大家都知道, 并发场景下, 有多个线程同时修改同一个共享变量可能会导致线程安全问题, 有下述方案可以解决

  • 加锁
    • 通过加锁, 让带代码线性排队执行, 例如synchronizedLock

在这里插入图片描述

  • ThreadLocal
    • ThreadLocal采用的是空间换时间
    • 将共享变量拷贝一份到线程的本地, 本地保存了共享变量的拷贝副本
    • 多线程对共享变量修改时, 实际上修改的是变量副本, 从而保证线程安全
      在这里插入图片描述

TL原理

TL结构

内存结构, 先看图
在这里插入图片描述

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. Thread类中, 有一个类型Thread.ThreadLocalMap的实例变量, 即每个线程都有自己的ThreadLocalMap
  2. ThreadLocalMap内部维护了Entry数组, 每个Entry代表一个完整对象
    • keyThreadLocal(并不是ThreadLocal本身, 而是它的弱引用)
    • valueThreadLocal的泛型对象值
  3. 线程隔离: 每个线程在往ThreadLocal里放值的时候,都会往自己ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离
  4. 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作为keyThreadLocalMap(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的实现原理

哪些点要回答到?

  1. 线程和ThreadLocalMap关系?
  2. Entry的K-V存储的是什么?
  3. 并发下怎么就线程隔离了?
  • 每个线程都有一个属于自己的ThreadLocalMap

    • Thread线程类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals
  • Entry的Key存ThreadLocal本身, Value存ThreadLocal泛型值

    • ThreadLocalMap内部维护着Entry数组,每个Entry代表一个完整的对象,keyThreadLocal本身,valueThreadLocal的泛型值
  • 每个线程读写操作的时, 都根据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) {// 初始化桶, 初始容量为16table = new Entry[INITIAL_CAPACITY];// hash+ 取模计算桶索引int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 在该桶位置放置一个新Entrytable[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,过程如下:

  1. i位置为null: 就初始化一个Entry对象放在位置i上
  2. 位置i已经有Entry对象: 如果这个Entry对象的key正好是即将设置的key,那么重新设置Entry中的value
  3. 位置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中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话(比如线程池的核心线程),这些keynullEntryvalue就会一直存在一条强引用链:Thread变量 -> Thread对象 -> ThreaLocalMap -> Entry -> value -> Object 永远无法回收, 造成内存泄漏。
在这里插入图片描述

内存泄漏的具体条件

在这里插入图片描述

总结上述, 可以发现导致内存泄露的概率非常低

  1. 只要ThreadLocal没被回收(使用时强引用不置null),那ThreadLocalMap Entry key的指向就不会在GC时断开被回收,也没有内存泄露一说法
  2. ThreadLocalMap是依附在Thread上的,只要Thread销毁,那ThreadLocalMap也会销毁, 非线程池环境下,也不会有长期性的内存泄露问题
  3. 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() {@Overridepublic 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 {// 100Mprivate 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考虑到上述线程存活周期较长的情况, 导致内存泄露的问题, 在在ThreadLocalget,set,remove方法,都会清除线程ThreadLocalMap里所有keynullvalue

看看源码是怎么做的

set()

ThreadLocalMapset数据(新增或者更新数据)分为好几种情况

**第一种情况:**槽位数据不为空,key值与当前ThreadLocal通过hash计算获取的key值一致

  • 直接更新该槽位的数据
    在这里插入图片描述

第二种情况: 通过hash计算后的槽位对应的Entry数据为空

  • 直接将数据放到该槽位即可
    在这里插入图片描述

第三种情况: 槽位数据不为空,往后遍历过程中,在找到Entrynull的槽位之前,没有遇到key过期的Entry

  • 遍历散列数组,线性往后查找

    • 如果找到Entrynull的槽位,则将数据放入该槽位中

在这里插入图片描述

  • 如果找到的key值相等数据,直接更新即可
    在这里插入图片描述

第四种情况: 槽位数据不为空,往后遍历过程中,在找到Entrynull的槽位之前,遇到key过期的Entry

大致步骤

  1. 初始化探测式清理扫描起始位置

    • 起始位置即staleSlot初始为index=6

      外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. staleSlot位置向迭代

    • 以当前staleSlot开始 向前迭代查找,检测是否有过期的Entry数据,如果有则更新过期数据起始扫描下标slotToExpunge。如果找到了过期的数据,继续向前迭代, 直到碰到Entrynull结束
  • slotToExpunge的作用用来判断当前过期槽位staleSlot之前是否还有过期元素, 后续会使用到的

    在这里插入图片描述

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

    在这里插入图片描述

  1. staleSlot位置向迭代

    • 没有没有Key相同的Entry

      • 迭代后没有找到Key相同的Entry

      在这里插入图片描述

      • stableSlotEntryValue置空,将传入的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();}
}

总结上述

什么情况下桶可以使用?

  1. k = key : 说明是替换操作, 可以使用
  2. k = null: 说明是过期桶, 执行替换逻辑, 占用过期桶
  3. Entry=null: 表示该桶没有存放Entry,直接使用

这里提一嘴replaceStaleEntry和cleanSomeSlots的区别

replaceStaleEntry

  • 触发时机: 调用新值时(即调用set方法), 发现 Entry 中的 Key 已经被垃圾回收器回收(即 Key 为 null),但是 Value 还存在时,会调用此方法进行替换
  • 目的: 将当前线程本地变量表中的这个无效(stale)Entry 替换为新的键值对,同时清理掉旧的、不再需要的 Value 对象引用,以防止内存泄漏
  • replaceStaleEntry 更侧重于在插入新值时立即处理遇到的无效Entry

cleanSomeSlots

  • 触发时机: 通用的清理方法, 通常在进行扩容、初始化或在某些操作后触发,用于清理整个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. 获取指定索引EntryEntry e = table[i];// 3. Entry匹配: 直接返回该Entryif (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的ThreadLocalThreadLocal<?> k = e.get();// 如果找到的键k等于传入的key,说明找到了匹配的Entry,返回这个Entryif (k == key)return e;// Entry的key为null,则表明没有外部引用,且被GC回收,是一个过期Entryif (k == null)expungeStaleEntry(i); //删除过期的Entryelse // 否则,使用nextIndex(i, len);方法计算下一个索引位置i = nextIndex(i, len);// 并更新当前条目e为新索引处的Entrye = tab[i];}// 3. 找不到匹配的Entry// 当循环结束仍没有找到与key相等的Entry时,表明哈希表中不存在与给定key关联的Entry,函数返回nullreturn 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的KeyThreadLocal<?> k = e.get();// 如果当前遍历到Entry的Key和方法传入的Key, 那么就说明这里是替换逻辑if (k == key) {// 将当前的Entry的Value更新为方法传入的Valuee.value = value;// 交换当前staleSlot位置中Entrytab[i] = tab[staleSlot];tab[staleSlot] = e;// 如果slotToExpunge == staleSlot, 说明replaceStaleEntry()一开始向前查找过期数据时并未找到过期的Entry数据, 接着向后查找过程中也未发现过期数据if (slotToExpunge == staleSlot) {// 修改slotToExpunge(开始探测式清理过期数据的下标)为当前循环的indexslotToExpunge = 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说明,一开始的向前查找数据并未找到过期的Entryif (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说明完往前迭代的时候, 发现了其它的过期的Entryif (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;// 将删除标记更新为trueremoved = 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=nullrehash计算index最近的节点

在这里插入图片描述

探测式清理干了什么?

  1. 清理过期的Entry
  2. 碰到正常数据,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-1tab[staleSlot].value = null;tab[staleSlot] = null;size--;// 3. 循环重新哈希,直到遇到nullEntry e;int i;// 从staleSlot的下一个槽位开始遍历,直到遇到空槽位(即Entry=null)为止for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) {// 对于每个非空槽位i,获取其中存储的ThreadLocal实例k,检查其是否为nullThreadLocal<?> 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=16table = new Entry[INITIAL_CAPACITY];// 计算key的indexint i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 将value设置到桶table[i] = new Entry(firstKey, firstValue);// size设置为1size = 1;// 设置桶的扩容阈值,即计算所得初始扩容阈值为10setThreshold(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和其长度oldLenEntry[] 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的KeyThreadLocal<?> k = e.get();// 如果key=null, 表明这个Entry是过期的,将Value置空(GC回收)if (k == null) {e.value = null;} else { // 如果key不为null,表示这是一个正常的Entry// 计算这个Entry在新table中位置hint 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+1count++;}}}// 更新扩容阈值, setThreshold中计算扩容阈值公式: 新阈值 = newLen * 2 / 3setThreshold(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代表正常数据,黄色块代表Entrykey值为null,已被垃圾回收红色块表示Entrynull, 准备存放的位置

ThreadLocalMap中使用了黄金分隔数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突

HashMap解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树

ThreadLocalMap并没有链表结构, 也就说不能像HashMap一样哈希冲突时在同一个槽位下挂载

在这里插入图片描述

结论

如果发生了Hash冲突, 那么就会线性查找, 一直找到Entrynull的槽位才会停止查找,将当前元素放入此槽位中。

在迭代的过程中, 比如遇到了Entry不为nullkey值相等的情况,还有Entry中的key值为null的情况等等都会有不同的处理, 在内存泄漏篇中的源码中已经详细阐述了

key是弱引用,GC回收会影响ThreadLocal的正常工作嘛?

ThreadLocalkey既然是弱引用会不会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的时候会被清除

也就说, 使用弱引用作为EntryKey,可以多一层保障:弱引用ThreadLocal不会轻易内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除

实际上,我们的内存泄漏的根本原因是,不再被使用的Entry,没有从线程的ThreadLocalMap中删除。一般删除不再使用的Entry有这两种方式:

  • 使用完ThreadLocal,手动调用remove(),把Entry从ThreadLocalMap中删除
  • ThreadLocalMap的自动清除机制去清除过期EntryThreadLocalMapget(),set()时都会触发对过期Entry的清除)
    • 这种方式不可靠, 如果依赖ThreadLocalMap的清除机制, 就很有可能发生内存泄漏
ThreadLocal和synchronized的区别?
  • 相同之处: ThreadLocalsynchronized关键字都用于处理多线程并发访问变量的问题

  • 不同之处

    • 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即可),这样我们在查看日志时候,就可以根据这个标记来区分调用链路

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赋值给子线程的inheritableThreadLocalsthis.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. 这里使用的是单例线程池,固定线程数是1
  2. 首次submit任务的时,线程池会初始化一个线程,创建的时候会调用构造方法初始化,此时会将父线程中ITL复制到子线程中, 所以第一次显示为逸一时,误一世
  3. 第二次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@Overridepublic <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@Overridepublic <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@Overridepublic 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;}@Overridepublic 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);// 回调模板方法beforeExecutedoExecuteCallback(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);// 移除子线程新增的TTLfor (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>, ?>>() {@Overrideprotected WeakHashMap<TransmittableThreadLocalCode<Object>, ?> initialValue() {// holder默认使用InheritableThreadLocal。初始化的时候会调用initialValue返回一个WeekHashMapreturn new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>();}@Overrideprotected WeakHashMap<TransmittableThreadLocalCode<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocalCode<Object>, ?> parentValue) {// 返回的是子线程在第一次get的时候的初始值,如果不重写,默认就是返回父线程的值return new WeakHashMap<TransmittableThreadLocalCode<Object>, Object>(parentValue);}
};
holder拷贝
  • TTL有一个静态内部类Transmitter ,专门用于操作TTL本地线程缓存的重放、恢复备份、清除等操作。下面以TtlRunnable作为一个入口进行分析
  1. 通过TtlRunnable.get(runnable)进行增强调用
// 调用执行线程的时候包裹(TtlRunnable.get
executor.execute(TtlRunnable.get(runnable));
  1. 会调用到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;
}
总结
  1. TTL通过增强Runnable,将原本位于构造方法的变量副本的传递,推迟到线程任务执行的时候,即在run()中,这样即使是使用线程池的线程,也能够在使用的时候将线程的变量副本继续传递下去

  2. 通过captured/replay/restor捕获、重放和回放机制,避免了在高并发情况下,线程池在CallerRunsPolicy拒绝策略下,启动的异步线程和主线程在同一线程内执行,因为子线程修改线程的变量副本从而导致业务数据混乱的问题

    • capture方法:抓取线程(线程A)的所有TTL
    • replay方法:在另一个线程(线程B)中,回放在capture方法中抓取的TTL值,并返回 回放前TTL值的备份
    • restore方法:恢复线程B执行replay方法之前的TTL值(即备份)

总结参考部分TTL的Issues

没有特别理解 capture replay restore 这样的方式的好处?

TTL值的抓取、回放和恢复方法(即CRR操作)

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区别

  1. ThreadLocal:单个线程生命周期强绑定,只能在某个线程的生命周期内对ThreadLocal进行存取,不能跨线程存取

  2. InheritableThreadLocal:在子线程创建的时候,父线程会把threadLocal拷贝到子线中(但是线程池的子线程不会频繁创建,就不会传递信息)

  3. TransmittableThreadLocal:解决了ITL中线程池无法传递线程本地副本的问题,在构造类似Runnable接口对象时进行初始化

参考资料

Java中的ThreadLocal

Java面试必问:ThreadLocal终极篇

⛳面试题-简述并分析ThreadLocalMap的key为什么是弱引用

系列八、key是弱引用,gc垃圾回收时会影响ThreadLocal正常工作吗

ThreadLocal是如何导致内存泄漏的

深入分析 ThreadLocal 内存泄漏问题

面试必备:ThreadLocal详解

Java面试必问,ThreadLocal终极篇

面试官:小伙子,听说你看过ThreadLocal源码?(万字图文深度解析ThreadLocal)

ThreadLocal就是这么简单

对ThreadLocal实现原理的一点思考

JAVA并发-自问自答学ThreadLocal

ThreadLocal夺命11连问

ThreadLocal的介绍+经典应用场景

Java的ThreadLocal,弱引用的Key使用后GC?

ThreadLocal的进化——InheritableThreadLocal

讲透 ThreadLocal 和 InheritableThreadLocal

从ThreadLocal谈到TransmittableThreadLocal,从使用到原理

TransmittableThreadLocal原理解析

还在为线程间上下文传递而烦恼,用TransmittableThreadLocal试试

一文吃透ThreadLocal的前世与今生

ThreadLocal你懂了,你还懂TransmittableThreadLocal嘛?

阿里开源的TransmittableThreadLocal的正确使用姿势

ThreadLocal的进化——TransmittableThreadLocal

待画图

TransmittableThreadLocal解决线程池本地变量问题,原来我一直理解错了❌

待重新研读

通过transmittable-thread-local源码理解线程池线程本地变量传递的原理

全链路追踪必备组件之 TransmittableThreadLocal 详解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/728580.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Service Mesh:如何为您的微服务架构带来可靠性和灵活性

在云原生架构中&#xff0c;Service Mesh 技术成为了微服务架构中不可或缺的一环。本文灸哥将和你一起探讨 Service Mesh 技术的原理、功能和实践&#xff0c;帮助架构师和开发人员更好地理解和应用这一关键技术。 1、Service Mesh 技术概述 Service Mesh 又称为服务网格&…

世界的本质是旋转(5)-在复平面上驱动软件无线电SDR发射BPSK波形

在上一篇文章中&#xff0c;我们介绍了复平面、拍照采样的一些思维实验。从本节开始&#xff0c;转入现实应用&#xff0c;通过控制复平面向量的位置&#xff0c;实现一个完整的BPSK全双工通信通道。 发射方&#xff1a;通过控制复平面向量在各个时刻的位置来携带信息的技术&a…

Axure RP 10:让原型设计更快、更直观、更智能 mac版

Axure RP 10是一款强大的原型设计工具&#xff0c;它能够帮助设计师快速创建高保真、交互式的原型&#xff0c;从而更好地展示和测试设计方案。这款软件凭借其直观易用的界面和丰富的功能&#xff0c;已经成为了许多设计师的首 选工具。 Axure RP 10 for Mac版软件获取 首先&a…

AI论文速读 | 【综述】城市计算中跨域数据融合的深度学习:分类、进展和展望

题目&#xff1a;Deep Learning for Cross-Domain Data Fusion in Urban Computing: Taxonomy, Advances, and Outlook 作者&#xff1a;Xingchen Zou, Yibo Yan, Xixuan Hao, Yuehong Hu, Haomin Wen&#xff08;温皓珉&#xff09;, Erdong Liu, Junbo Zhang&#xff08;张钧…

进程之舞:操作系统中的启动、状态转换与唤醒艺术

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua&#xff0c;在这里我会分享我的知识和经验。&#x…

解决QT cc1plus.exe: error: out of memory allocating

QT中增加资源文件过大时&#xff0c;会编译不过&#xff0c;报错&#xff1a; cc1plus.exe: out of memory allocating 1073745919 bytes 使用qrc资源文件&#xff0c;也就是在QT的工程中添加资源文件&#xff0c;就是添加的资源文件&#xff08;如qrc.cpp&#xff09;会直接被…

简明固体物理--晶体的形成与晶体结构的描述

简明固体物理-国防科技大学 chapter 1 Formation of Crystal Contents and roadmapQuantum Mechanics and atomic structureElectronsOld quantum theoryMethod of Quantum MechanicsDistributing functions of micro-particles BindingCrystal structure and typical crystal…

Go-Gin-example 第五部分 加入swagger

上一节链接 swagger 为什么要用swagger 问题起源于 前后端分离&#xff0c; 后端&#xff1a;后端控制层&#xff0c;服务层&#xff0c;数据访问层【后端团队】前端&#xff1a;前端控制层&#xff0c;视图层&#xff0c;【前端团队】 所以产生问题&#xff1a;前后端联调…

Keepalived+LVS构建高可用集群

目录 一、Keepalive基础介绍 1. Keepalive与VRRP 2. VRRP相关技术 3. 工作原理 4. 模块 5. 架构 6. 安装 7. Keepalived 相关文件 7.1 配置组成 7.2 全局配置 7.3 VRRP实例配置&#xff08;lvs调度器&#xff09; 7.4 虚拟服务器与真实服务器配置 二、Keepalived…

HTML静态网页成品作业(HTML+CSS)——花主题介绍网页设计制作(1个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有1个页面。 二、作品演示 三、代…

C语言:基于单链表实现的泊车管理系统

一、需求 &#xff08;1&#xff09;管理员方账号登录&#xff1b; &#xff08;2&#xff09;车位管理显示&#xff1a;车位状态&#xff1b; &#xff08;3&#xff09;收费管理&#xff1a;小轿车 5元/小时&#xff0c;面包车6元/小时&#xff0c;大货车或客车7元/小时&a…

ChatGPT提示技巧——零,一和少量示例提示

ChatGPT提示技巧——零&#xff0c;一和少量示例提示 ​ 零样本(zero-shot)、少样本(few-shot)和单样本(one-shot)提示是用于在最少或没有示例的情况下从ChatGPT生成文本的技巧。这些技巧用于当某个具体任务有限定数据的时候或者任务是新的并且没有很好的定义的时候。 提示格…

设计模式之——简单工厂模式

上图为简单工厂模式的架构图。 1&#xff0c;产品&#xff08;Product&#xff09; 将会对接口进行声明。 2&#xff0c;具体产品&#xff08;Concrete Products&#xff09;是产品接口的不同实现。 3&#xff0c;创建者&#xff08;Concrete Creators&#xff09;将会重写基…

TCP传输收发

TCP通信: TCP发端: socket connect send recv close TCP收端: socket bind listen accept send recv close 1.connect int connect(int sockfd, const struct sockaddr *addr, socklen_t ad…

20个Python函数程序实例

前面介绍的函数太简单了&#xff1a; 以下是 20 个不同的 Python 函数实例 下面深入一点点&#xff1a; 以下是20个稍微深入一点的&#xff0c;使用Python语言定义并调用函数的示例程序&#xff1a; 20个函数实例 简单函数调用 def greet():print("Hello!")greet…

css-vxe-form-item中输入框加自定义按钮(校验位置错误)

1.浮动错误效果 提示内容不对 2.不使用浮动&#xff0c;使用行内块元素 代码如下 <vxe-form-item title"yoyo:" field"assembleWorkNo" span"8"><template #default><vxe-input style"width:70%;display:inline-block;&quo…

全天候购药系统(微信小程序+web后台管理)

PurchaseApplet 全天候购药系统&#xff08;微信小程序web后台管理&#xff09; 传统线下购药方式存在无法全天候向用户提供购药服务&#xff0c;无法随时提供诊疗服务等问题。为此&#xff0c;运用软件工程开发规范&#xff0c;充分调研建立需求模型&#xff0c;编写开发文档…

Java输入和输出处理

一、Java I/O 文件、内存、键盘--->程序--->文件、内存、控制台 二、文件 相关记录或放在一起的数据的集合 思考&#xff1a; Java程序如何访问文件属性&#xff1f; 解答&#xff1a; Java API:java.io.File类 三、File类 File类的常用方法 方法名称说明boole…

maven项目结构管理统一项目配置操作

一、maven分模块开发 Maven 分模块开发 1.先创建父工程&#xff0c;pom.xml文件中&#xff0c;打包方式为pom 2.然后里面有许多子工程 3.我要对父工程的maven对所有子工程进行操作 二、解读maven的结构 1.模块1 <groupId>org.TS</groupId><artifactId>TruthS…