diff --git a/docs/cs-basics/network/network-attack-means.md b/docs/cs-basics/network/network-attack-means.md index 86a1d7f6..dfacb45b 100644 --- a/docs/cs-basics/network/network-attack-means.md +++ b/docs/cs-basics/network/network-attack-means.md @@ -367,7 +367,7 @@ MD5 可以用来生成一个 128 位的消息摘要,它是目前应用比较 **SM3** -国密算法**SM3**。加密强度和 SHA-256算法 相差不多。主要是受到了国家的支持。 +国密算法**SM3**。加密强度和 SHA-256 算法 相差不多。主要是受到了国家的支持。 **总结**: diff --git a/docs/java/concurrent/java-concurrent-questions-02.md b/docs/java/concurrent/java-concurrent-questions-02.md index 143fd5a4..45a1fc83 100644 --- a/docs/java/concurrent/java-concurrent-questions-02.md +++ b/docs/java/concurrent/java-concurrent-questions-02.md @@ -559,7 +559,7 @@ public class SynchronizedDemo2 { `synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取而代之的是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 -**不过两者的本质都是对对象监视器 monitor 的获取。** +**不过,两者的本质都是对对象监视器 monitor 的获取。** 相关推荐:[Java 锁与线程的那些事 - 有赞技术团队](https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/) 。 @@ -683,9 +683,9 @@ public class SynchronizedDemo { 关于 **等待可中断** 的补充: -> `lockInterruptibly()` 会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待 +> `lockInterruptibly()` 会让获取锁的线程在阻塞等待的过程中可以响应中断,即当前线程在获取锁的时候,发现锁被其他线程持有,就会阻塞等待。 > -> 在阻塞等待的过程中,如果其他线程中断当前线程 「 `interrupt()` 」,就会抛出 `InterruptedException` 异常,可以捕获该异常,做一些处理操作 +> 在阻塞等待的过程中,如果其他线程中断当前线程 `interrupt()` ,就会抛出 `InterruptedException` 异常,可以捕获该异常,做一些处理操作。 > > 为了更好理解这个方法,借用 Stack Overflow 上的一个案例,可以更好地理解 `lockInterruptibly()` 可以响应中断: > @@ -754,17 +754,11 @@ public class SynchronizedDemo { > **为什么需要 `tryLock(timeout)` 这个功能呢?** > -> 假设这样一种场景:有一个加载缓存数据的任务在某个时间点多个线程同时要来执行,为了并发安全,通过锁来控制只有一个线程可以执行该任务。 +> `tryLock(timeout)` 方法尝试在指定的超时时间内获取锁。如果成功获取锁,则返回 `true`;如果在锁可用之前超时,则返回 `false`。此功能在以下几种场景中非常有用: > -> 假设大量线程同时来执行该任务,由于需要穿行执行,因此大量线程都进入阻塞队列等待获取锁 -> -> 当第一个线程拿到锁,执行完任务之后,此时后边的线程都不需要执行该任务了,但是由于没有这个超时功能,导致后边的线程还需要在队列中阻塞等待获取锁,再一个个进入同步代码块,发现任务已经执行过了,不需要自己再执行了,之后再退出释放锁,退出同步代码块。 -> -> 因此就需要一个支持超时的功能,`tryLock(timeout)` 的作用就是 **将大量线程的串行操作转为并行操作** ,当大量线程等待时间已经超过了指定的超时时间,直接返回 false,表示获取锁失败,不需要大量的线程串行排队等待获取锁。 -> -> ![image-20241208153800259](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/image-20241208153800259.png) -> -> 这里 `tryLock(timeout)` 的情况只是举一个特殊的情况,其实是参考了分布式环境下,更新 Redis 缓存时会出现这种情况,但是在分布式环境下肯定不会使用 synchronized ,因此这里主要是举个例子说一下 tryLock(timeout) 的作用! +> - **防止死锁:** 在复杂的锁场景中,`tryLock(timeout)` 可以通过允许线程在合理的时间内放弃并重试来帮助防止死锁。 +> - **提高响应速度:** 防止线程无限期阻塞。 +> - **处理时间敏感的操作:** 对于具有严格时间限制的操作,`tryLock(timeout)` 允许线程在无法及时获取锁时继续执行替代操作。 ### 可中断锁和不可中断锁有什么区别? diff --git a/docs/java/concurrent/java-concurrent-questions-03.md b/docs/java/concurrent/java-concurrent-questions-03.md index dca2289a..e9312763 100644 --- a/docs/java/concurrent/java-concurrent-questions-03.md +++ b/docs/java/concurrent/java-concurrent-questions-03.md @@ -106,53 +106,40 @@ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { ### ⭐️ThreadLocal 内存泄露问题是怎么导致的? -`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。 +`ThreadLocal` 内存泄漏的根本原因在于其内部实现机制。 -如果 ThreadLocal 使用不当,就会发生内存泄漏,发生内存泄漏需要满足 2 个条件: +通过上面的内容我们已经知道:每个线程维护一个名为 `ThreadLocalMap` 的 map。 当你使用 `ThreadLocal` 存储值时,实际上是将值存储在当前线程的 `ThreadLocalMap` 中,其中 `ThreadLocal` 实例本身作为 key,而你要存储的值作为 value。 -- ThreadLocal 被定义为方法的局部变量,从而导致 ThreadLocalMap 中的 key 在 GC 之后变为 null。 -- 线程持续存活(比如线程处在线程池中),导致线程内部的 ThreadLocalMap 对象一直未被回收。 +`ThreadLocalMap` 的 `key` 和 `value` 引用机制: -**通过案例解释为什么会内存泄漏:** +- **key 是弱引用**:`ThreadLocalMap` 中的 key 是 `ThreadLocal` 的弱引用 (`WeakReference>`)。 这意味着,如果 `ThreadLocal` 实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致 `ThreadLocalMap` 中对应的 key 变为 `null`。 +- **value 是强引用**:`ThreadLocalMap` 中的 value 是强引用。 即使 key 被回收(变为 `null`),value 仍然存在于 `ThreadLocalMap` 中,被强引用,不会被回收。 -假设将 ThreadLocal 定义为方法中的 **局部变量** ,那么当线程进入该方法的时候,就会将 ThreadLocal 的引用给加载到线程的 **栈** 中,假设为 ThreadLocalRef。 +```java +static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; -如下图所示,在线程栈 Stack 中,有两个变量,ThreadLocalRef 和 CurrentThreadRef,分别指向了声明的局部变量 ThreadLocal ,以及当前执行的线程内部的 ThreadLocalMap 变量。 + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` -![image-20241210225928979](https://11laile-note-img.oss-cn-beijing.aliyuncs.com/image-20241210225928979.png) +当 `ThreadLocal` 实例失去强引用后,其对应的 value 仍然存在于 `ThreadLocalMap` 中,因为 `Entry` 对象强引用了它。如果线程持续存活(例如线程池中的线程),`ThreadLocalMap` 也会一直存在,导致 key 为 `null` 的 entry 无法被垃圾回收,机会造成内存泄漏。 -当线程执行完该方法之后,就会将该方法的局部变量从栈中删除。 +也就是说,内存泄漏的发生需要同时满足两个条件: -因此 Stack 线程栈中的 ThreadLocalRef 变量就会被弹出栈,此时 ThreadLocal 变量的强引用消失了,现在只有 Entry 中的 key 对它进行弱引用。 +1. `ThreadLocal` 实例不再被强引用; +2. 线程持续存活,导致 `ThreadLocalMap` 长期存在。 -那么这个 ThreadLocal 变量就会被垃圾回收器给回收掉,导致 Entry 中的 key 为 null,同时 value 指向了对 Object 的强引用。 - -同时假设当前这个线程一直存活,那么 Thread 内部的 ThreadLocalMap 变量就不会被回收,因此 ThreadLocalMap 内部的 Entry 的 value 指向的 Object 对象一直不会被回收,如下图(对线程的引用不一定在 Stack 栈中,还有可能在方法区,这里画在 Stack 栈中是为了方便理解): - -![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) - -**因此内存泄漏的发生需要满足 2 个条件:** - -1、ThreadLocal 定义为方法内的局部变量,当方法执行完毕之后,ThreadLocal 被回收,导致无法通过 ThreadLocal 访问到 ThreadLocalMap 内部的 value。 - -2、Stack 线程栈内部的 CurrentThreadRef 引用指向的线程 **一直存活** ,导致线程内部的 ThreadLocalMap 也无法被回收,从而导致 Entry 的 value 一直存在指向 Object 的强引用,导致 Object 对象无法回收,出现内存泄漏。 - -JDK 团队也考虑到了这种情况,因此在设计 ThreadLocal 时还添加了清除 ThreadLocalMap 中 key 为 null 的 value 的功能,避免内存泄漏。这是在设计阶段为了避免内存泄漏而采取的措施,而我们使用的时候要保持良好的编程规范,正确定义 ThreadLocal,并且手动 remove,避免内存泄漏的发生。 - -结论:如果 `ThreadLocal` 被定义为方法的局部变量,并且线程一直存活,就会导致内存泄漏的发生。 +虽然 `ThreadLocalMap` 在 `get()`, `set()` 和 `remove()` 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。 **如何避免内存泄漏的发生?** -遵循阿里巴巴的开发规范: - -- 将 ThreadLocal 变量定义成 `private static final` ,这样就一直存在 ThreadLocal 的强引用,也能保证任何时候都能通过 ThreadLocal 的访问到 Entry 的 value 值,进而清除掉。 -- 每次使用完 ThreadLocal 都主动调用它的 remove() 方法清除数据。 - -**弱引用介绍:** - -> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 -> -> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 +1. 在使用完 `ThreadLocal` 后,务必调用 `remove()` 方法。 这是最安全和最推荐的做法。 `remove()` 方法会从 `ThreadLocalMap` 中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将 `ThreadLocal` 定义为 `static final`,也强烈建议在每次使用后调用 `remove()`。 +2. 在线程池等线程复用的场景下,使用 `try-finally` 块可以确保即使发生异常,`remove()` 方法也一定会被执行。 ## 线程池 diff --git a/docs/system-design/security/advantages-and-disadvantages-of-jwt.md b/docs/system-design/security/advantages-and-disadvantages-of-jwt.md index b177fa0e..bbb6d197 100644 --- a/docs/system-design/security/advantages-and-disadvantages-of-jwt.md +++ b/docs/system-design/security/advantages-and-disadvantages-of-jwt.md @@ -17,7 +17,7 @@ JWT 不是银弹,也有很多缺陷,很多时候并不是最优的选择。 ### 无状态 -JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 +JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 JWT 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。 不过,也正是由于 JWT 的无状态,也导致了它最大的缺点:**不可控!**