mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-25 02:27:10 +08:00
Update 万字详解ThreadLocal关键字.md
This commit is contained in:
parent
2a4f95308d
commit
2e81334161
@ -1,19 +1,19 @@
|
||||
> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.im/post/5eacc1c75188256d976df748](https://juejin.im/post/5eacc1c75188256d976df748)。
|
||||
> 本文来自一枝花算不算浪漫投稿, 原文地址:[https://juejin.im/post/5eacc1c75188256d976df748](https://juejin.im/post/5eacc1c75188256d976df748)。
|
||||
|
||||
### 前言
|
||||
|
||||

|
||||
|
||||
**全文共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`
|
||||
|
||||
@ -231,7 +229,7 @@ public class ThreadLocal<T> {
|
||||
|
||||
每当创建一个`ThreadLocal`对象,这个`ThreadLocal.nextHashCode` 这个值就会增长 `0x61c88647` 。
|
||||
|
||||
这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**。`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**。
|
||||
这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**。`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**。
|
||||
|
||||
我们自己可以尝试下:
|
||||
|
||||
@ -239,7 +237,7 @@ public class ThreadLocal<T> {
|
||||
|
||||
可以看到产生的哈希码分布很均匀,这里不去细纠**斐波那契**具体算法,感兴趣的可以自行查阅相关资料。
|
||||
|
||||
### `ThreadLocalMap` Hash冲突
|
||||
### `ThreadLocalMap` Hash 冲突
|
||||
|
||||
> **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据**,**灰色块**代表`Entry`的`key`值为`null`,**已被垃圾回收**。**白色块**表示`Entry`为`null`。
|
||||
|
||||
@ -247,22 +245,21 @@ public class ThreadLocal<T> {
|
||||
|
||||
`HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。
|
||||
|
||||
而`ThreadLocalMap`中并没有链表结构,所以这里不能适用`HashMap`解决冲突的方式了。
|
||||
而 `ThreadLocalMap` 中并没有链表结构,所以这里不能使用 `HashMap` 解决冲突的方式了。
|
||||
|
||||

|
||||
|
||||
如上图所示,如果我们插入一个`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<T> {
|
||||
|
||||

|
||||
|
||||
遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key值相等**的数据,直接更新即可。
|
||||
遍历散列数组,线性往后查找,如果找到`Entry`为`null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key 值相等**的数据,直接更新即可。
|
||||
|
||||
**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry`为`null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,一到了`index=7`的槽位数据`Entry`的`key=null`:
|
||||
|
||||

|
||||
|
||||
散列数组下标为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**:
|
||||
|
||||

|
||||
|
||||
以当前节点(`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 数据:**
|
||||
|
||||

|
||||
|
||||
从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,找到后更新`Entry`的值并交换`staleSlot`元素的位置(`staleSlot`位置为过期元素),更新`Entry`数据,然后开始进行过期`Entry`的清理工作,如下图所示:
|
||||
|
||||

|
||||
|
||||
|
||||
**向后遍历过程中,如果没有找到相同key值的Entry数据:**
|
||||
向后遍历过程中,如果没有找到相同 key 值的 Entry 数据:
|
||||
|
||||

|
||||
|
||||
@ -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) {
|
||||
@ -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)
|
||||
|
||||

|
||||
|
||||
如上图,`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`
|
||||
|
||||

|
||||
|
||||
@ -529,7 +526,7 @@ if (slotToExpunge != staleSlot)
|
||||
|
||||

|
||||
|
||||
执行完第二步后,index=4的元素挪到index=3的槽位中。
|
||||
执行完第二步后,index=4 的元素挪到 index=3 的槽位中。
|
||||
|
||||
继续往后迭代检查,碰到正常数据,计算该数据位置是否偏移,如果被偏移,则重新计算`slot`位置,目的是让正常数据尽可能存放在正确位置或离正确位置更近的位置
|
||||
|
||||
@ -686,7 +683,7 @@ private void resize() {
|
||||
|
||||

|
||||
|
||||
我们以`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`中。
|
||||
|
||||

|
||||
|
||||
@ -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,7 +873,7 @@ public class LogInterceptor extends HandlerInterceptorAdapter {
|
||||
}
|
||||
```
|
||||
|
||||
#### 线程池异步调用,requestId传递
|
||||
#### 线程池异步调用,requestId 传递
|
||||
|
||||
因为`MDC`是基于`ThreadLocal`去实现的,异步过程中,子线程并没有办法获取到父线程`ThreadLocal`存储的数据,所以这里可以自定义线程池执行器,修改其中的`run()`方法:
|
||||
|
||||
@ -903,11 +900,6 @@ public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用MQ发送消息给第三方系统
|
||||
|
||||
在MQ发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。
|
||||
|
||||
|
||||
|
||||
|
||||
#### 使用 MQ 发送消息给第三方系统
|
||||
|
||||
在 MQ 发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。
|
||||
|
Loading…
x
Reference in New Issue
Block a user