diff --git a/docs/cs-basics/network/other-network-questions2.md b/docs/cs-basics/network/other-network-questions2.md index 9d193f49..dcfd4f46 100644 --- a/docs/cs-basics/network/other-network-questions2.md +++ b/docs/cs-basics/network/other-network-questions2.md @@ -73,9 +73,21 @@ tag: 🐛 修正(参见 [issue#1915](https://github.com/Snailclimb/JavaGuide/issues/1915)): -HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** 。 +HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议** : -此变化解决了 HTTP/2.0 中存在的队头阻塞问题。队头阻塞是指在 HTTP/2.0 中,多个 HTTP 请求和响应共享一个 TCP 连接,如果其中一个请求或响应因为网络拥塞或丢包而被阻塞,那么后续的请求或响应也无法发送,导致整个连接的效率降低。这是由于 HTTP/2.0 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。HTTP/3.0 在一定程度上解决了队头阻塞问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。 +- **HTTP/1.x 和 HTTP/2.0**:这两个版本的 HTTP 协议都明确建立在 TCP 之上。TCP 提供了可靠的、面向连接的传输,确保数据按序、无差错地到达,这对于网页内容的正确展示非常重要。发送 HTTP 请求前,需要先通过 TCP 的三次握手建立连接。 +- **HTTP/3.0**:这是一个重大的改变。HTTP/3 弃用了 TCP,转而使用 QUIC 协议,而 QUIC 是构建在 UDP 之上的。 + +![http-3-implementation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-3-implementation.png) + +**为什么 HTTP/3 要做这个改变呢?主要有两大原因:** + +1. 解决队头阻塞 (Head-of-Line Blocking,简写:HOL blocking) 问题。 +2. 减少连接建立的延迟。 + +下面我们来详细介绍这两大优化。 + +在 HTTP/2 中,虽然可以在一个 TCP 连接上并发传输多个请求/响应流(多路复用),但 TCP 本身的特性(保证有序、可靠)意味着如果其中一个流的某个 TCP 报文丢失或延迟,整个 TCP 连接都会被阻塞,等待该报文重传。这会导致所有在这个 TCP 连接上的 HTTP/2 流都受到影响,即使其他流的数据包已经到达。**QUIC (运行在 UDP 上) 解决了这个问题**。QUIC 内部实现了自己的多路复用和流控制机制。不同的 HTTP 请求/响应流在 QUIC 层面是真正独立的。如果一个流的数据包丢失,它只会阻塞该流,而不会影响同一 QUIC 连接上的其他流(本质上是多路复用+轮询),大大提高了并发传输的效率。 除了解决队头阻塞问题,HTTP/3.0 还可以减少握手过程的延迟。在 HTTP/2.0 中,如果要建立一个安全的 HTTPS 连接,需要经过 TCP 三次握手和 TLS 握手: diff --git a/docs/java/basis/unsafe.md b/docs/java/basis/unsafe.md index efd1337d..fff31af8 100644 --- a/docs/java/basis/unsafe.md +++ b/docs/java/basis/unsafe.md @@ -516,11 +516,94 @@ private void increment(int x){ 1 2 3 4 5 6 7 8 9 ``` -在上面的例子中,使用两个线程去修改`int`型属性`a`的值,并且只有在`a`的值等于传入的参数`x`减一时,才会将`a`的值变为`x`,也就是实现对`a`的加一的操作。流程如下所示: +如果你把上面这段代码贴到 IDE 中运行,会发现并不能得到目标输出结果。有朋友已经在 Github 上指出了这个问题:[issue#2650](https://github.com/Snailclimb/JavaGuide/issues/2650)。下面是修正后的代码: + +```java +private volatile int a = 0; // 共享变量,初始值为 0 +private static final Unsafe unsafe; +private static final long fieldOffset; + +static { + try { + // 获取 Unsafe 实例 + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + unsafe = (Unsafe) theUnsafe.get(null); + // 获取 a 字段的内存偏移量 + fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a")); + } catch (Exception e) { + throw new RuntimeException("Failed to initialize Unsafe or field offset", e); + } +} + +public static void main(String[] args) { + CasTest casTest = new CasTest(); + + Thread t1 = new Thread(() -> { + for (int i = 1; i <= 4; i++) { + casTest.incrementAndPrint(i); + } + }); + + Thread t2 = new Thread(() -> { + for (int i = 5; i <= 9; i++) { + casTest.incrementAndPrint(i); + } + }); + + t1.start(); + t2.start(); + + // 等待线程结束,以便观察完整输出 (可选,用于演示) + try { + t1.join(); + t2.join(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } +} + +// 将递增和打印操作封装在一个原子性更强的方法内 +private void incrementAndPrint(int targetValue) { + while (true) { + int currentValue = a; // 读取当前 a 的值 + // 只有当 a 的当前值等于目标值的前一个值时,才尝试更新 + if (currentValue == targetValue - 1) { + if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) { + // CAS 成功,说明成功将 a 更新为 targetValue + System.out.print(targetValue + " "); + break; // 成功更新并打印后退出循环 + } + // 如果 CAS 失败,意味着在读取 currentValue 和执行 CAS 之间,a 的值被其他线程修改了, + // 此时 currentValue 已经不是 a 的最新值,需要重新读取并重试。 + } + // 如果 currentValue != targetValue - 1,说明还没轮到当前线程更新, + // 或者已经被其他线程更新超过了,让出CPU给其他线程机会。 + // 对于严格顺序递增的场景,如果 current > targetValue - 1,可能意味着逻辑错误或死循环, + // 但在此示例中,我们期望线程能按顺序执行。 + Thread.yield(); // 提示CPU调度器可以切换线程,减少无效自旋 + } +} +``` + +在上述例子中,我们创建了两个线程,它们都尝试修改共享变量 a。每个线程在调用 `incrementAndPrint(targetValue)` 方法时: + +1. 会先读取 a 的当前值 `currentValue`。 +2. 检查 `currentValue` 是否等于 `targetValue - 1` (即期望的前一个值)。 +3. 如果条件满足,则调用`unsafe.compareAndSwapInt()` 尝试将 `a` 从 `currentValue` 更新到 `targetValue`。 +4. 如果 CAS 操作成功(返回 true),则打印 `targetValue` 并退出循环。 +5. 如果 CAS 操作失败,或者 `currentValue` 不满足条件,则当前线程会继续循环(自旋),并通过 `Thread.yield()` 尝试让出 CPU,直到成功更新并打印或者条件满足。 + +这种机制确保了每个数字(从 1 到 9)只会被成功设置并打印一次,并且是按顺序进行的。 ![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144939826.png) -需要注意的是,在调用`compareAndSwapInt`方法后,会直接返回`true`或`false`的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在`AtomicInteger`类的设计中,也是采用了将`compareAndSwapInt`的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。 +需要注意的是: + +1. **自旋逻辑:** `compareAndSwapInt` 方法本身只执行一次比较和交换操作,并立即返回结果。因此,为了确保操作最终成功(在值符合预期的情况下),我们需要在代码中显式地实现自旋逻辑(如 `while(true)` 循环),不断尝试直到 CAS 操作成功。 +2. **`AtomicInteger` 的实现:** JDK 中的 `java.util.concurrent.atomic.AtomicInteger` 类内部正是利用了类似的 CAS 操作和自旋逻辑来实现其原子性的 `getAndIncrement()`, `compareAndSet()` 等方法。直接使用 `AtomicInteger` 通常是更安全、更推荐的做法,因为它封装了底层的复杂性。 +3. **ABA 问题:** CAS 操作本身存在 ABA 问题(一个值从 A 变为 B,再变回 A,CAS 检查时会认为值没有变过)。在某些场景下,如果值的变化历史很重要,可能需要使用 `AtomicStampedReference` 来解决。但在本例的简单递增场景中,ABA 问题通常不构成影响。 +4. **CPU 消耗:** 长时间的自旋会消耗 CPU 资源。在竞争激烈或条件长时间不满足的情况下,可以考虑加入更复杂的退避策略(如 `Thread.sleep()` 或 `LockSupport.parkNanos()`)来优化。 ### 线程调度