1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-25 02:27:10 +08:00

Update 万字详解ThreadLocal关键字.md

This commit is contained in:
guide 2021-08-14 10:05:36 +08:00
parent 2a4f95308d
commit 2e81334161

View File

@ -23,8 +23,6 @@
### 目录
**注明:** 本文源码基于`JDK 1.8`
### `ThreadLocal`代码演示
@ -91,7 +89,6 @@ size: 0
- **弱引用**:使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
- **虚引用**:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
接着再来看下代码,我们使用反射的方式来看看`GC``ThreadLocal`中的数据情况:(下面代码来源自https://blog.csdn.net/thewindkee/article/details/103726942 本地运行演示 GC 回收场景)
```java
@ -140,6 +137,7 @@ public class ThreadLocalDemo {
```
结果如下:
```java
弱引用key:java.lang.ThreadLocal@433619b6,值:abc
弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12
@ -247,11 +245,10 @@ public class ThreadLocal<T> {
`HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。
`ThreadLocalMap`中并没有链表结构,所以这里不能适用`HashMap`解决冲突的方式了。
`ThreadLocalMap` 中并没有链表结构,所以这里不能使用 `HashMap` 解决冲突的方式了。
![](./images/thread-local/7.png)
如上图所示,如果我们插入一个`value=27`的数据,通过 `hash` 计算后应该落入第 4 个槽位中,而槽位 4 已经有了 `Entry` 数据。
此时就会线性向后查找,一直找到 `Entry``null` 的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了 `Entry` 不为 `null``key` 值相等的情况,还有 `Entry` 中的 `key` 值为 `null` 的情况等等都会有不同的处理,后面会一一详细讲解。
@ -308,10 +305,7 @@ public class ThreadLocal<T> {
从当前节点`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,6 +381,7 @@ private static int prevIndex(int i, int len) {
```
接着看剩下`for`循环中的逻辑:
1. 遍历当前`key`值对应的桶中`Entry`数据为空,这说明散列数组这里没有数据冲突,跳出`for`循环,直接`set`数据到对应的桶中
2. 如果`key`值对应的桶中`Entry`数据不为空
2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回
@ -494,6 +490,7 @@ tab[staleSlot] = new Entry(key, value);
```
最后判断除了`staleSlot`以外,还发现了其他过期的`slot`数据,就要开启清理数据的逻辑:
```java
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
@ -724,10 +721,8 @@ private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
}
```
### `ThreadLocalMap`过期 key 的启发式清理流程
上面多次提及到`ThreadLocalMap`过期可以的两种清理方式:**探测式清理(expungeStaleEntry())**、**启发式清理(cleanSomeSlots())**
探测式清理是以当前`Entry` 往后清理,遇到值为`null`则结束清理,属于**线性探测清理**。
@ -816,7 +811,7 @@ private void init(ThreadGroup g, Runnable target, String name,
我们现在项目中日志记录用的是`ELK+Logstash`,最后在`Kibana`中进行展示和检索。
现在都是分布式系统统一对外提供服务项目间调用的关系可以通过traceId来关联但是不同项目之间如何传递`traceId`呢?
现在都是分布式系统统一对外提供服务,项目间调用的关系可以通过 `traceId` 来关联,但是不同项目之间如何传递 `traceId` 呢?
这里我们使用 `org.slf4j.MDC` 来实现此功能,内部就是通过 `ThreadLocal` 来实现的,具体实现如下:
@ -833,6 +828,7 @@ private void init(ThreadGroup g, Runnable target, String name,
#### Feign 远程调用解决方案
**服务发送请求:**
```java
@Component
@Slf4j
@ -849,6 +845,7 @@ public class FeignInvokeInterceptor implements RequestInterceptor {
```
**服务接收请求:**
```java
@Slf4j
@Component
@ -906,8 +903,3 @@ public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
#### 使用 MQ 发送消息给第三方系统
在 MQ 发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。