Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit a269e1c

Browse files
[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‎

Lines changed: 1 addition & 1 deletion
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‎

Lines changed: 7 additions & 13 deletions
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‎

Lines changed: 22 additions & 35 deletions
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‎

Lines changed: 1 addition & 1 deletion
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)

AltStyle によって変換されたページ (->オリジナル) /