Skip to content

Commit a269e1c

Browse files
committed
[docs update] ThreadLocal内存泄露原因完善
1 parent ab46fee commit a269e1c

File tree

4 files changed

+31
-50
lines changed

4 files changed

+31
-50
lines changed

docs/cs-basics/network/network-attack-means.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,7 @@ MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较
367367

368368
**SM3**
369369

370-
国密算法**SM3**。加密强度和 SHA-256算法 相差不多。主要是受到了国家的支持。
370+
国密算法**SM3**。加密强度和 SHA-256 算法 相差不多。主要是受到了国家的支持。
371371

372372
**总结**
373373

docs/java/concurrent/java-concurrent-questions-02.md

+7-13
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ public class SynchronizedDemo2 {
559559

560560
`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。
561561

562-
**不过两者的本质都是对对象监视器 monitor 的获取。**
562+
**不过,两者的本质都是对对象监视器 monitor 的获取。**
563563

564564
相关推荐:[Java 锁与线程的那些事 - 有赞技术团队](https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/)
565565

@@ -683,9 +683,9 @@ public class SynchronizedDemo {
683683
684684
关于 **等待可中断** 的补充:
685685

686-
> `lockInterruptibly()` 会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待
686+
> `lockInterruptibly()` 会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待
687687
>
688-
> 在阻塞等待的过程中,如果其他线程中断当前线程 `interrupt()` ,就会抛出 `InterruptedException` 异常,可以捕获该异常,做一些处理操作
688+
> 在阻塞等待的过程中,如果其他线程中断当前线程 `interrupt()` ,就会抛出 `InterruptedException` 异常,可以捕获该异常,做一些处理操作
689689
>
690690
> 为了更好理解这个方法,借用 Stack Overflow 上的一个案例,可以更好地理解 `lockInterruptibly()` 可以响应中断:
691691
>
@@ -754,17 +754,11 @@ public class SynchronizedDemo {
754754
755755
> **为什么需要 `tryLock(timeout)` 这个功能呢?**
756756
>
757-
> 假设这样一种场景:有一个加载缓存数据的任务在某个时间点多个线程同时要来执行,为了并发安全,通过锁来控制只有一个线程可以执行该任务。
757+
> `tryLock(timeout)` 方法尝试在指定的超时时间内获取锁。如果成功获取锁,则返回 `true`;如果在锁可用之前超时,则返回 `false`。此功能在以下几种场景中非常有用:
758758
>
759-
> 假设大量线程同时来执行该任务,由于需要穿行执行,因此大量线程都进入阻塞队列等待获取锁
760-
>
761-
> 当第一个线程拿到锁,执行完任务之后,此时后边的线程都不需要执行该任务了,但是由于没有这个超时功能,导致后边的线程还需要在队列中阻塞等待获取锁,再一个个进入同步代码块,发现任务已经执行过了,不需要自己再执行了,之后再退出释放锁,退出同步代码块。
762-
>
763-
> 因此就需要一个支持超时的功能,`tryLock(timeout)` 的作用就是 **将大量线程的串行操作转为并行操作** ,当大量线程等待时间已经超过了指定的超时时间,直接返回 false,表示获取锁失败,不需要大量的线程串行排队等待获取锁。
764-
>
765-
> ![image-20241208153800259](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/image-20241208153800259.png)
766-
>
767-
> 这里 `tryLock(timeout)` 的情况只是举一个特殊的情况,其实是参考了分布式环境下,更新 Redis 缓存时会出现这种情况,但是在分布式环境下肯定不会使用 synchronized ,因此这里主要是举个例子说一下 tryLock(timeout) 的作用!
759+
> - **防止死锁:** 在复杂的锁场景中,`tryLock(timeout)` 可以通过允许线程在合理的时间内放弃并重试来帮助防止死锁。
760+
> - **提高响应速度:** 防止线程无限期阻塞。
761+
> - **处理时间敏感的操作:** 对于具有严格时间限制的操作,`tryLock(timeout)` 允许线程在无法及时获取锁时继续执行替代操作。
768762
769763
### 可中断锁和不可中断锁有什么区别?
770764

docs/java/concurrent/java-concurrent-questions-03.md

+22-35
Original file line numberDiff line numberDiff line change
@@ -106,53 +106,40 @@ ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
106106

107107
### ⭐️ThreadLocal 内存泄露问题是怎么导致的?
108108

109-
`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用
109+
`ThreadLocal` 内存泄漏的根本原因在于其内部实现机制
110110

111-
如果 ThreadLocal 使用不当,就会发生内存泄漏,发生内存泄漏需要满足 2 个条件:
111+
通过上面的内容我们已经知道:每个线程维护一个名为 `ThreadLocalMap` 的 map。 当你使用 `ThreadLocal` 存储值时,实际上是将值存储在当前线程的 `ThreadLocalMap` 中,其中 `ThreadLocal` 实例本身作为 key,而你要存储的值作为 value。
112112

113-
- ThreadLocal 被定义为方法的局部变量,从而导致 ThreadLocalMap 中的 key 在 GC 之后变为 null。
114-
- 线程持续存活(比如线程处在线程池中),导致线程内部的 ThreadLocalMap 对象一直未被回收。
113+
`ThreadLocalMap``key``value` 引用机制:
115114

116-
**通过案例解释为什么会内存泄漏:**
115+
- **key 是弱引用**`ThreadLocalMap` 中的 key 是 `ThreadLocal` 的弱引用 (`WeakReference<ThreadLocal<?>>`)。 这意味着,如果 `ThreadLocal` 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 `ThreadLocalMap` 中对应的 key 变为 `null`
116+
- **value 是强引用**`ThreadLocalMap` 中的 value 是强引用。 即使 key 被回收(变为 `null`),value 仍然存在于 `ThreadLocalMap` 中,被强引用,不会被回收。
117117

118-
假设将 ThreadLocal 定义为方法中的 **局部变量** ,那么当线程进入该方法的时候,就会将 ThreadLocal 的引用给加载到线程的 **** 中,假设为 ThreadLocalRef。
119-
120-
如下图所示,在线程栈 Stack 中,有两个变量,ThreadLocalRef 和 CurrentThreadRef,分别指向了声明的局部变量 ThreadLocal ,以及当前执行的线程内部的 ThreadLocalMap 变量。
121-
122-
![image-20241210225928979](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/image-20241210225928979.png)
123-
124-
当线程执行完该方法之后,就会将该方法的局部变量从栈中删除。
125-
126-
因此 Stack 线程栈中的 ThreadLocalRef 变量就会被弹出栈,此时 ThreadLocal 变量的强引用消失了,现在只有 Entry 中的 key 对它进行弱引用。
127-
128-
那么这个 ThreadLocal 变量就会被垃圾回收器给回收掉,导致 Entry 中的 key 为 null,同时 value 指向了对 Object 的强引用。
129-
130-
同时假设当前这个线程一直存活,那么 Thread 内部的 ThreadLocalMap 变量就不会被回收,因此 ThreadLocalMap 内部的 Entry 的 value 指向的 Object 对象一直不会被回收,如下图(对线程的引用不一定在 Stack 栈中,还有可能在方法区,这里画在 Stack 栈中是为了方便理解):
131-
132-
![ThreadLocal 结构和内存泄漏](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/ThreadLocal%20%E7%BB%93%E6%9E%84%E5%92%8C%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.jpg)
118+
```java
119+
static class Entry extends WeakReference<ThreadLocal<?>> {
120+
/** The value associated with this ThreadLocal. */
121+
Object value;
133122

134-
**因此内存泄漏的发生需要满足 2 个条件:**
123+
Entry(ThreadLocal<?> k, Object v) {
124+
super(k);
125+
value = v;
126+
}
127+
}
128+
```
135129

136-
1、ThreadLocal 定义为方法内的局部变量,当方法执行完毕之后,ThreadLocal 被回收,导致无法通过 ThreadLocal 访问到 ThreadLocalMap 内部的 value
130+
`ThreadLocal` 实例失去强引用后,其对应的 value 仍然存在于 `ThreadLocalMap` 中,因为 `Entry` 对象强引用了它。如果线程持续存活(例如线程池中的线程),`ThreadLocalMap` 也会一直存在,导致 key 为 `null` 的 entry 无法被垃圾回收,机会造成内存泄漏
137131

138-
2、Stack 线程栈内部的 CurrentThreadRef 引用指向的线程 **一直存活** ,导致线程内部的 ThreadLocalMap 也无法被回收,从而导致 Entry 的 value 一直存在指向 Object 的强引用,导致 Object 对象无法回收,出现内存泄漏。
132+
也就是说,内存泄漏的发生需要同时满足两个条件:
139133

140-
JDK 团队也考虑到了这种情况,因此在设计 ThreadLocal 时还添加了清除 ThreadLocalMap 中 key 为 null 的 value 的功能,避免内存泄漏。这是在设计阶段为了避免内存泄漏而采取的措施,而我们使用的时候要保持良好的编程规范,正确定义 ThreadLocal,并且手动 remove,避免内存泄漏的发生。
134+
1. `ThreadLocal` 实例不再被强引用;
135+
2. 线程持续存活,导致 `ThreadLocalMap` 长期存在。
141136

142-
结论:如果 `ThreadLocal` 被定义为方法的局部变量,并且线程一直存活,就会导致内存泄漏的发生
137+
虽然 `ThreadLocalMap``get()`, `set()``remove()` 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠
143138

144139
**如何避免内存泄漏的发生?**
145140

146-
遵循阿里巴巴的开发规范:
147-
148-
- 将 ThreadLocal 变量定义成 `private static final` ,这样就一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的访问到 Entry 的 value 值,进而清除掉。
149-
- 每次使用完 ThreadLocal 都主动调用它的 remove() 方法清除数据。
150-
151-
**弱引用介绍:**
152-
153-
> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
154-
>
155-
> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
141+
1. 在使用完 `ThreadLocal` 后,务必调用 `remove()` 方法。 这是最安全和最推荐的做法。 `remove()` 方法会从 `ThreadLocalMap` 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 `ThreadLocal` 定义为 `static final`,也强烈建议在每次使用后调用 `remove()`
142+
2. 在线程池等线程复用的场景下,使用 `try-finally` 块可以确保即使发生异常,`remove()` 方法也一定会被执行。
156143

157144
## 线程池
158145

docs/system-design/security/advantages-and-disadvantages-of-jwt.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ JWT 不是银弹,也有很多缺陷,很多时候并不是最优的选择。
1717

1818
### 无状态
1919

20-
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
20+
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 JWT 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
2121

2222
不过,也正是由于 JWT 的无状态,也导致了它最大的缺点:**不可控!**
2323

0 commit comments

Comments
 (0)