diff --git a/docs/java/multi-thread/万字详解ThreadLocal关键字.md b/docs/java/multi-thread/万字详解ThreadLocal关键字.md index 3650ddbb..a7c79807 100644 --- a/docs/java/multi-thread/万字详解ThreadLocal关键字.md +++ b/docs/java/multi-thread/万字详解ThreadLocal关键字.md @@ -1,19 +1,19 @@ -> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.im/post/5eacc1c75188256d976df748](https://juejin.im/post/5eacc1c75188256d976df748)。 +> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.im/post/5eacc1c75188256d976df748](https://juejin.im/post/5eacc1c75188256d976df748)。 ### 前言 ![](./images/thread-local/1.png) -**全文共10000+字,31张图,这篇文章同样耗费了不少的时间和精力才创作完成,原创不易,请大家点点关注+在看,感谢。** +**全文共 10000+字,31 张图,这篇文章同样耗费了不少的时间和精力才创作完成,原创不易,请大家点点关注+在看,感谢。** 对于`ThreadLocal`,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下: -- `ThreadLocal`的key是**弱引用**,那么在 `ThreadLocal`.get()的时候,发生**GC**之后,key是否为**null**? +- `ThreadLocal`的 key 是**弱引用**,那么在 `ThreadLocal`.get()的时候,发生**GC**之后,key 是否为**null**? - `ThreadLocal`中`ThreadLocalMap`的**数据结构**? -- `ThreadLocalMap`的**Hash算法**? -- `ThreadLocalMap`中**Hash冲突**如何解决? +- `ThreadLocalMap`的**Hash 算法**? +- `ThreadLocalMap`中**Hash 冲突**如何解决? - `ThreadLocalMap`的**扩容机制**? -- `ThreadLocalMap`中**过期key的清理机制**?**探测式清理**和**启发式清理**流程? +- `ThreadLocalMap`中**过期 key 的清理机制**?**探测式清理**和**启发式清理**流程? - `ThreadLocalMap.set()`方法实现原理? - `ThreadLocalMap.get()`方法实现原理? - 项目中`ThreadLocal`使用情况?遇到的坑? @@ -23,8 +23,6 @@ ### 目录 - - **注明:** 本文源码基于`JDK 1.8` ### `ThreadLocal`代码演示 @@ -80,19 +78,18 @@ size: 0 我们还要注意`Entry`, 它的`key`是`ThreadLocal k` ,继承自`WeakReference`, 也就是我们常说的弱引用类型。 -### GC 之后key是否为null? +### GC 之后 key 是否为 null? 回应开头的那个问题, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`? 为了搞清楚这个问题,我们需要搞清楚`Java`的**四种引用类型**: -- **强引用**:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候 -- **软引用**:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收 -- **弱引用**:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收 +- **强引用**:我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候 +- **软引用**:使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收 +- **弱引用**:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收 - **虚引用**:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知 - -接着再来看下代码,我们使用反射的方式来看看`GC`后`ThreadLocal`中的数据情况:(下面代码来源自:https://blog.csdn.net/thewindkee/article/details/103726942 本地运行演示GC回收场景) +接着再来看下代码,我们使用反射的方式来看看`GC`后`ThreadLocal`中的数据情况:(下面代码来源自:https://blog.csdn.net/thewindkee/article/details/103726942 本地运行演示 GC 回收场景) ```java public class ThreadLocalDemo { @@ -140,6 +137,7 @@ public class ThreadLocalDemo { ``` 结果如下: + ```java 弱引用key:java.lang.ThreadLocal@433619b6,值:abc 弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12 @@ -192,7 +190,7 @@ void createMap(Thread t, T firstValue) { 主要的核心逻辑还是在`ThreadLocalMap`中的,一步步往下看,后面还有更详细的剖析。 -### `ThreadLocalMap` Hash算法 +### `ThreadLocalMap` Hash 算法 既然是`Map`结构,那么`ThreadLocalMap`当然也要实现自己的`hash`算法来解决散列表数组冲突问题。 @@ -200,7 +198,7 @@ void createMap(Thread t, T firstValue) { int i = key.threadLocalHashCode & (len-1); ``` -`ThreadLocalMap`中`hash`算法很简单,这里`i`就是当前key在散列表中对应的数组下标位置。 +`ThreadLocalMap`中`hash`算法很简单,这里`i`就是当前 key 在散列表中对应的数组下标位置。 这里最关键的就是`threadLocalHashCode`值的计算,`ThreadLocal`中有一个属性为`HASH_INCREMENT = 0x61c88647` @@ -215,7 +213,7 @@ public class ThreadLocal { private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } - + static class ThreadLocalMap { ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; @@ -231,7 +229,7 @@ public class ThreadLocal { 每当创建一个`ThreadLocal`对象,这个`ThreadLocal.nextHashCode` 这个值就会增长 `0x61c88647` 。 -这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**。`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**。 +这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**。`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**。 我们自己可以尝试下: @@ -239,7 +237,7 @@ public class ThreadLocal { 可以看到产生的哈希码分布很均匀,这里不去细纠**斐波那契**具体算法,感兴趣的可以自行查阅相关资料。 -### `ThreadLocalMap` Hash冲突 +### `ThreadLocalMap` Hash 冲突 > **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据**,**灰色块**代表`Entry`的`key`值为`null`,**已被垃圾回收**。**白色块**表示`Entry`为`null`。 @@ -247,22 +245,21 @@ public class ThreadLocal { `HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。 -而`ThreadLocalMap`中并没有链表结构,所以这里不能适用`HashMap`解决冲突的方式了。 +而 `ThreadLocalMap` 中并没有链表结构,所以这里不能使用 `HashMap` 解决冲突的方式了。 ![](./images/thread-local/7.png) +如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入第 4 个槽位中,而槽位 4 已经有了 `Entry` 数据。 -如上图所示,如果我们插入一个`value=27`的数据,通过`hash`计算后应该落入第4个槽位中,而槽位4已经有了`Entry`数据。 +此时就会线性向后查找,一直找到 `Entry` 为 `null` 的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了 `Entry` 不为 `null` 且 `key` 值相等的情况,还有 `Entry` 中的 `key` 值为 `null` 的情况等等都会有不同的处理,后面会一一详细讲解。 -此时就会线性向后查找,一直找到`Entry`为`null`的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了`Entry`不为`null`且`key`值相等的情况,还有`Entry`中的`key`值为`null`的情况等等都会有不同的处理,后面会一一详细讲解。 - -这里还画了一个`Entry`中的`key`为`null`的数据(**Entry=2的灰色块数据**),因为`key`值是**弱引用**类型,所以会有这种数据存在。在`set`过程中,如果遇到了`key`过期的`Entry`数据,实际上是会进行一轮**探测式清理**操作的,具体操作方式后面会讲到。 +这里还画了一个`Entry`中的`key`为`null`的数据(**Entry=2 的灰色块数据**),因为`key`值是**弱引用**类型,所以会有这种数据存在。在`set`过程中,如果遇到了`key`过期的`Entry`数据,实际上是会进行一轮**探测式清理**操作的,具体操作方式后面会讲到。 ### `ThreadLocalMap.set()`详解 #### `ThreadLocalMap.set()`原理图解 -看完了`ThreadLocal` **hash算法**后,我们再来看`set`是如何实现的。 +看完了`ThreadLocal` **hash 算法**后,我们再来看`set`是如何实现的。 往`ThreadLocalMap`中`set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说说明。 @@ -282,36 +279,33 @@ public class ThreadLocal { ![](./images/thread-local/11.png) -遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key值相等**的数据,直接更新即可。 +遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key 值相等**的数据,直接更新即可。 **第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,一到了`index=7`的槽位数据`Entry`的`key=null`: ![](./images/thread-local/12.png) -散列数组下标为7位置对应的`Entry`数据`key`为`null`,表明此数据`key`值已经被垃圾回收掉了,此时就会执行`replaceStaleEntry()`方法,该方法含义是**替换过期数据的逻辑**,以**index=7**位起点开始遍历,进行探测式数据清理工作。 +散列数组下标为 7 位置对应的`Entry`数据`key`为`null`,表明此数据`key`值已经被垃圾回收掉了,此时就会执行`replaceStaleEntry()`方法,该方法含义是**替换过期数据的逻辑**,以**index=7**位起点开始遍历,进行探测式数据清理工作。 初始化探测式清理过期数据扫描的开始位置:`slotToExpunge = staleSlot = 7` 以当前`staleSlot`开始 向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标`slotToExpunge`。`for`循环迭代,直到碰到`Entry`为`null`结束。 -如果找到了过期的数据,继续向前迭代,直到遇到`Entry=null`的槽位才停止迭代,如下图所示,**slotToExpunge被更新为0**: +如果找到了过期的数据,继续向前迭代,直到遇到`Entry=null`的槽位才停止迭代,如下图所示,**slotToExpunge 被更新为 0**: ![](./images/thread-local/13.png) -以当前节点(`index=7`)向前迭代,检测是否有过期的`Entry`数据,如果有则更新`slotToExpunge`值。碰到`null`则结束探测。以上图为例`slotToExpunge`被更新为0。 +以当前节点(`index=7`)向前迭代,检测是否有过期的`Entry`数据,如果有则更新`slotToExpunge`值。碰到`null`则结束探测。以上图为例`slotToExpunge`被更新为 0。 上面向前迭代的操作是为了更新探测清理过期数据的起始下标`slotToExpunge`的值,这个值在后面会讲解,它是用来判断当前过期槽位`staleSlot`之前是否还有过期元素。 -接着开始以`staleSlot`位置(index=7)向后迭代,**如果找到了相同key值的Entry数据:** +接着开始以`staleSlot`位置(index=7)向后迭代,**如果找到了相同 key 值的 Entry 数据:** ![](./images/thread-local/14.png) 从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,找到后更新`Entry`的值并交换`staleSlot`元素的位置(`staleSlot`位置为过期元素),更新`Entry`数据,然后开始进行过期`Entry`的清理工作,如下图所示: -![Yu4oWT.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba9af057e1e?w=1336&h=361&f=png&s=63049) - - -**向后遍历过程中,如果没有找到相同key值的Entry数据:** +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/java-guide-blog/view.png)向后遍历过程中,如果没有找到相同 key 值的 Entry 数据: ![](./images/thread-local/15.png) @@ -367,6 +361,7 @@ int i = key.threadLocalHashCode & (len-1); ``` 什么情况下桶才是可以使用的呢? + 1. `k = key` 说明是替换操作,可以使用 2. 碰到一个过期的桶,执行替换逻辑,占用过期桶 3. 查找过程中,碰到桶中`Entry=null`的情况,直接使用 @@ -386,16 +381,17 @@ private static int prevIndex(int i, int len) { ``` 接着看剩下`for`循环中的逻辑: + 1. 遍历当前`key`值对应的桶中`Entry`数据为空,这说明散列数组这里没有数据冲突,跳出`for`循环,直接`set`数据到对应的桶中 2. 如果`key`值对应的桶中`Entry`数据不为空 -2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回 -2.2 如果`key = null`,说明当前桶位置的`Entry`是过期数据,执行`replaceStaleEntry()`方法(核心方法),然后返回 + 2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回 + 2.2 如果`key = null`,说明当前桶位置的`Entry`是过期数据,执行`replaceStaleEntry()`方法(核心方法),然后返回 3. `for`循环执行完毕,继续往下执行说明向后迭代的过程中遇到了`entry`为`null`的情况 -3.1 在`Entry`为`null`的桶中创建一个新的`Entry`对象 -3.2 执行`++size`操作 + 3.1 在`Entry`为`null`的桶中创建一个新的`Entry`对象 + 3.2 执行`++size`操作 4. 调用`cleanSomeSlots()`做一次启发式清理工作,清理散列数组中`Entry`的`key`过期的数据 -4.1 如果清理工作完成后,未清理到任何数据,且`size`超过了阈值(数组长度的2/3),进行`rehash()`操作 -4.2 `rehash()`中会先进行一轮探测式清理,清理过期`key`,清理完成后如果**size >= threshold - threshold / 4**,就会执行真正的扩容逻辑(扩容逻辑往后看) + 4.1 如果清理工作完成后,未清理到任何数据,且`size`超过了阈值(数组长度的 2/3),进行`rehash()`操作 + 4.2 `rehash()`中会先进行一轮探测式清理,清理过期`key`,清理完成后如果**size >= threshold - threshold / 4**,就会执行真正的扩容逻辑(扩容逻辑往后看) 接着重点看下`replaceStaleEntry()`方法,`replaceStaleEntry()`方法提供替换过期数据的功能,我们可以对应上面**第四种情况**的原理图来再回顾下,具体代码如下: @@ -446,7 +442,7 @@ private void replaceStaleEntry(ThreadLocal key, Object value, } ``` -`slotToExpunge`表示开始探测式清理过期数据的开始下标,默认从当前的`staleSlot`开始。以当前的`staleSlot`开始,向前迭代查找,找到没有过期的数据,`for`循环一直碰到`Entry`为`null`才会结束。如果向前找到了过期数据,更新探测清理过期数据的开始下标为i,即`slotToExpunge=i` +`slotToExpunge`表示开始探测式清理过期数据的开始下标,默认从当前的`staleSlot`开始。以当前的`staleSlot`开始,向前迭代查找,找到没有过期的数据,`for`循环一直碰到`Entry`为`null`才会结束。如果向前找到了过期数据,更新探测清理过期数据的开始下标为 i,即`slotToExpunge=i` ```java for (int i = prevIndex(staleSlot, len); @@ -460,7 +456,7 @@ for (int i = prevIndex(staleSlot, len); ``` 接着开始从`staleSlot`向后查找,也是碰到`Entry`为`null`的桶结束。 -如果迭代过程中,**碰到k == key**,这说明这里是替换逻辑,替换新数据并且交换当前`staleSlot`位置。如果`slotToExpunge == staleSlot`,这说明`replaceStaleEntry()`一开始向前查找过期数据时并未找到过期的`Entry`数据,接着向后查找过程中也未发现过期数据,修改开始探测式清理过期数据的下标为当前循环的index,即`slotToExpunge = i`。最后调用`cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);`进行启发式过期数据清理。 +如果迭代过程中,**碰到 k == key**,这说明这里是替换逻辑,替换新数据并且交换当前`staleSlot`位置。如果`slotToExpunge == staleSlot`,这说明`replaceStaleEntry()`一开始向前查找过期数据时并未找到过期的`Entry`数据,接着向后查找过程中也未发现过期数据,修改开始探测式清理过期数据的下标为当前循环的 index,即`slotToExpunge = i`。最后调用`cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);`进行启发式过期数据清理。 ```java if (k == key) { @@ -468,7 +464,7 @@ if (k == key) { tab[i] = tab[staleSlot]; tab[staleSlot] = e; - + if (slotToExpunge == staleSlot) slotToExpunge = i; @@ -479,7 +475,7 @@ if (k == key) { `cleanSomeSlots()`和`expungeStaleEntry()`方法后面都会细讲,这两个是和清理相关的方法,一个是过期`key`相关`Entry`的启发式清理(`Heuristically scan`),另一个是过期`key`相关`Entry`的探测式清理。 -**如果k != key**则会接着往下走,`k == null`说明当前遍历的`Entry`是一个过期数据,`slotToExpunge == staleSlot`说明,一开始的向前查找数据并未找到过期的`Entry`。如果条件成立,则更新`slotToExpunge` 为当前位置,这个前提是前驱节点扫描时未发现过期数据。 +**如果 k != key**则会接着往下走,`k == null`说明当前遍历的`Entry`是一个过期数据,`slotToExpunge == staleSlot`说明,一开始的向前查找数据并未找到过期的`Entry`。如果条件成立,则更新`slotToExpunge` 为当前位置,这个前提是前驱节点扫描时未发现过期数据。 ```java if (k == null && slotToExpunge == staleSlot) @@ -494,12 +490,13 @@ tab[staleSlot] = new Entry(key, value); ``` 最后判断除了`staleSlot`以外,还发现了其他过期的`slot`数据,就要开启清理数据的逻辑: + ```java if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); ``` -### `ThreadLocalMap`过期key的探测式清理流程 +### `ThreadLocalMap`过期 key 的探测式清理流程 上面我们有提及`ThreadLocalMap`的两种过期`key`数据清理方式:**探测式清理**和**启发式清理**。 @@ -507,7 +504,7 @@ if (slotToExpunge != staleSlot) ![](./images/thread-local/18.png) -如上图,`set(27)` 经过hash计算后应该落到`index=4`的桶中,由于`index=4`桶已经有了数据,所以往后迭代最终数据放入到`index=7`的桶中,放入后一段时间后`index=5`中的`Entry`数据`key`变为了`null` +如上图,`set(27)` 经过 hash 计算后应该落到`index=4`的桶中,由于`index=4`桶已经有了数据,所以往后迭代最终数据放入到`index=7`的桶中,放入后一段时间后`index=5`中的`Entry`数据`key`变为了`null` ![](./images/thread-local/19.png) @@ -529,7 +526,7 @@ if (slotToExpunge != staleSlot) ![](./images/thread-local/22.png) -执行完第二步后,index=4的元素挪到index=3的槽位中。 +执行完第二步后,index=4 的元素挪到 index=3 的槽位中。 继续往后迭代检查,碰到正常数据,计算该数据位置是否偏移,如果被偏移,则重新计算`slot`位置,目的是让正常数据尽可能存放在正确位置或离正确位置更近的位置 @@ -581,7 +578,7 @@ if (k == null) { e.value = null; tab[i] = null; size--; -} +} ``` 如果`key`没有过期,重新计算当前`key`的下标位置是不是当前槽位下标位置,如果不是,那么说明产生了`hash`冲突,此时以新计算出来正确的槽位位置往后迭代,找到最近一个可以存放`entry`的位置。 @@ -686,7 +683,7 @@ private void resize() { ![](./images/thread-local/27.png) -我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是4,而`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。 +我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是 4,而`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。 迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移,此时继续往后迭代,到`index = 6`的时候即找到了`key`值相等的`Entry`数据,如下图所示: @@ -724,9 +721,7 @@ private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { } ``` - -### `ThreadLocalMap`过期key的启发式清理流程 - +### `ThreadLocalMap`过期 key 的启发式清理流程 上面多次提及到`ThreadLocalMap`过期可以的两种清理方式:**探测式清理(expungeStaleEntry())**、**启发式清理(cleanSomeSlots())** @@ -760,7 +755,7 @@ private boolean cleanSomeSlots(int i, int n) { 我们使用`ThreadLocal`的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。 -为了解决这个问题,JDK中还有一个`InheritableThreadLocal`类,我们来看一个例子: +为了解决这个问题,JDK 中还有一个`InheritableThreadLocal`类,我们来看一个例子: ```java public class InheritableThreadLocalDemo { @@ -816,11 +811,11 @@ private void init(ThreadGroup g, Runnable target, String name, 我们现在项目中日志记录用的是`ELK+Logstash`,最后在`Kibana`中进行展示和检索。 -现在都是分布式系统统一对外提供服务,项目间调用的关系可以通过traceId来关联,但是不同项目之间如何传递`traceId`呢? +现在都是分布式系统统一对外提供服务,项目间调用的关系可以通过 `traceId` 来关联,但是不同项目之间如何传递 `traceId` 呢? -这里我们使用`org.slf4j.MDC`来实现此功能,内部就是通过`ThreadLocal`来实现的,具体实现如下: +这里我们使用 `org.slf4j.MDC` 来实现此功能,内部就是通过 `ThreadLocal` 来实现的,具体实现如下: -当前端发送请求到**服务A**时,**服务A**会生成一个类似`UUID`的`traceId`字符串,将此字符串放入当前线程的`ThreadLocal`中,在调用**服务B**的时候,将`traceId`写入到请求的`Header`中,**服务B**在接收请求时会先判断请求的`Header`中是否有`traceId`,如果存在则写入自己线程的`ThreadLocal`中。 +当前端发送请求到**服务 A**时,**服务 A**会生成一个类似`UUID`的`traceId`字符串,将此字符串放入当前线程的`ThreadLocal`中,在调用**服务 B**的时候,将`traceId`写入到请求的`Header`中,**服务 B**在接收请求时会先判断请求的`Header`中是否有`traceId`,如果存在则写入自己线程的`ThreadLocal`中。 ![](./images/thread-local/30.png) @@ -830,9 +825,10 @@ private void init(ThreadGroup g, Runnable target, String name, 针对于这些场景,我们都可以有相应的解决方案,如下所示 -#### Feign远程调用解决方案 +#### Feign 远程调用解决方案 **服务发送请求:** + ```java @Component @Slf4j @@ -849,6 +845,7 @@ public class FeignInvokeInterceptor implements RequestInterceptor { ``` **服务接收请求:** + ```java @Slf4j @Component @@ -876,13 +873,13 @@ public class LogInterceptor extends HandlerInterceptorAdapter { } ``` -#### 线程池异步调用,requestId传递 +#### 线程池异步调用,requestId 传递 因为`MDC`是基于`ThreadLocal`去实现的,异步过程中,子线程并没有办法获取到父线程`ThreadLocal`存储的数据,所以这里可以自定义线程池执行器,修改其中的`run()`方法: ```java public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { - + @Override public void execute(Runnable runnable) { Map context = MDC.getCopyOfContextMap(); @@ -903,11 +900,6 @@ public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { } ``` -#### 使用MQ发送消息给第三方系统 - -在MQ发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。 - - - - +#### 使用 MQ 发送消息给第三方系统 +在 MQ 发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。