Skip to content

Commit b1867d3

Browse files
committed
fix: cas 示例代码修正
1 parent dbdd3aa commit b1867d3

File tree

2 files changed

+99
-4
lines changed

2 files changed

+99
-4
lines changed

docs/cs-basics/network/other-network-questions2.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,21 @@ tag:
7373

7474
🐛 修正(参见 [issue#1915](https://github.com/Snailclimb/JavaGuide/issues/1915)):
7575

76-
HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议**
76+
HTTP/3.0 之前是基于 TCP 协议的,而 HTTP/3.0 将弃用 TCP,改用 **基于 UDP 的 QUIC 协议**
7777

78-
此变化解决了 HTTP/2.0 中存在的队头阻塞问题。队头阻塞是指在 HTTP/2.0 中,多个 HTTP 请求和响应共享一个 TCP 连接,如果其中一个请求或响应因为网络拥塞或丢包而被阻塞,那么后续的请求或响应也无法发送,导致整个连接的效率降低。这是由于 HTTP/2.0 在单个 TCP 连接上使用了多路复用,受到 TCP 拥塞控制的影响,少量的丢包就可能导致整个 TCP 连接上的所有流被阻塞。HTTP/3.0 在一定程度上解决了队头阻塞问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其数据流不受影响(本质上是多路复用+轮询)。
78+
- **HTTP/1.x 和 HTTP/2.0**:这两个版本的 HTTP 协议都明确建立在 TCP 之上。TCP 提供了可靠的、面向连接的传输,确保数据按序、无差错地到达,这对于网页内容的正确展示非常重要。发送 HTTP 请求前,需要先通过 TCP 的三次握手建立连接。
79+
- **HTTP/3.0**:这是一个重大的改变。HTTP/3 弃用了 TCP,转而使用 QUIC 协议,而 QUIC 是构建在 UDP 之上的。
80+
81+
![http-3-implementation](https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-3-implementation.png)
82+
83+
**为什么 HTTP/3 要做这个改变呢?主要有两大原因:**
84+
85+
1. 解决队头阻塞 (Head-of-Line Blocking,简写:HOL blocking) 问题。
86+
2. 减少连接建立的延迟。
87+
88+
下面我们来详细介绍这两大优化。
89+
90+
在 HTTP/2 中,虽然可以在一个 TCP 连接上并发传输多个请求/响应流(多路复用),但 TCP 本身的特性(保证有序、可靠)意味着如果其中一个流的某个 TCP 报文丢失或延迟,整个 TCP 连接都会被阻塞,等待该报文重传。这会导致所有在这个 TCP 连接上的 HTTP/2 流都受到影响,即使其他流的数据包已经到达。**QUIC (运行在 UDP 上) 解决了这个问题**。QUIC 内部实现了自己的多路复用和流控制机制。不同的 HTTP 请求/响应流在 QUIC 层面是真正独立的。如果一个流的数据包丢失,它只会阻塞该流,而不会影响同一 QUIC 连接上的其他流(本质上是多路复用+轮询),大大提高了并发传输的效率。
7991

8092
除了解决队头阻塞问题,HTTP/3.0 还可以减少握手过程的延迟。在 HTTP/2.0 中,如果要建立一个安全的 HTTPS 连接,需要经过 TCP 三次握手和 TLS 握手:
8193

docs/java/basis/unsafe.md

+85-2
Original file line numberDiff line numberDiff line change
@@ -516,11 +516,94 @@ private void increment(int x){
516516
1 2 3 4 5 6 7 8 9
517517
```
518518

519-
在上面的例子中,使用两个线程去修改`int`型属性`a`的值,并且只有在`a`的值等于传入的参数`x`减一时,才会将`a`的值变为`x`,也就是实现对`a`的加一的操作。流程如下所示:
519+
如果你把上面这段代码贴到 IDE 中运行,会发现并不能得到目标输出结果。有朋友已经在 Github 上指出了这个问题:[issue#2650](https://github.com/Snailclimb/JavaGuide/issues/2650)。下面是修正后的代码:
520+
521+
```java
522+
private volatile int a = 0; // 共享变量,初始值为 0
523+
private static final Unsafe unsafe;
524+
private static final long fieldOffset;
525+
526+
static {
527+
try {
528+
// 获取 Unsafe 实例
529+
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
530+
theUnsafe.setAccessible(true);
531+
unsafe = (Unsafe) theUnsafe.get(null);
532+
// 获取 a 字段的内存偏移量
533+
fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
534+
} catch (Exception e) {
535+
throw new RuntimeException("Failed to initialize Unsafe or field offset", e);
536+
}
537+
}
538+
539+
public static void main(String[] args) {
540+
CasTest casTest = new CasTest();
541+
542+
Thread t1 = new Thread(() -> {
543+
for (int i = 1; i <= 4; i++) {
544+
casTest.incrementAndPrint(i);
545+
}
546+
});
547+
548+
Thread t2 = new Thread(() -> {
549+
for (int i = 5; i <= 9; i++) {
550+
casTest.incrementAndPrint(i);
551+
}
552+
});
553+
554+
t1.start();
555+
t2.start();
556+
557+
// 等待线程结束,以便观察完整输出 (可选,用于演示)
558+
try {
559+
t1.join();
560+
t2.join();
561+
} catch (InterruptedException e) {
562+
Thread.currentThread().interrupt();
563+
}
564+
}
565+
566+
// 将递增和打印操作封装在一个原子性更强的方法内
567+
private void incrementAndPrint(int targetValue) {
568+
while (true) {
569+
int currentValue = a; // 读取当前 a 的值
570+
// 只有当 a 的当前值等于目标值的前一个值时,才尝试更新
571+
if (currentValue == targetValue - 1) {
572+
if (unsafe.compareAndSwapInt(this, fieldOffset, currentValue, targetValue)) {
573+
// CAS 成功,说明成功将 a 更新为 targetValue
574+
System.out.print(targetValue + " ");
575+
break; // 成功更新并打印后退出循环
576+
}
577+
// 如果 CAS 失败,意味着在读取 currentValue 和执行 CAS 之间,a 的值被其他线程修改了,
578+
// 此时 currentValue 已经不是 a 的最新值,需要重新读取并重试。
579+
}
580+
// 如果 currentValue != targetValue - 1,说明还没轮到当前线程更新,
581+
// 或者已经被其他线程更新超过了,让出CPU给其他线程机会。
582+
// 对于严格顺序递增的场景,如果 current > targetValue - 1,可能意味着逻辑错误或死循环,
583+
// 但在此示例中,我们期望线程能按顺序执行。
584+
Thread.yield(); // 提示CPU调度器可以切换线程,减少无效自旋
585+
}
586+
}
587+
```
588+
589+
在上述例子中,我们创建了两个线程,它们都尝试修改共享变量 a。每个线程在调用 `incrementAndPrint(targetValue)` 方法时:
590+
591+
1. 会先读取 a 的当前值 `currentValue`
592+
2. 检查 `currentValue` 是否等于 `targetValue - 1` (即期望的前一个值)。
593+
3. 如果条件满足,则调用`unsafe.compareAndSwapInt()` 尝试将 `a``currentValue` 更新到 `targetValue`
594+
4. 如果 CAS 操作成功(返回 true),则打印 `targetValue` 并退出循环。
595+
5. 如果 CAS 操作失败,或者 `currentValue` 不满足条件,则当前线程会继续循环(自旋),并通过 `Thread.yield()` 尝试让出 CPU,直到成功更新并打印或者条件满足。
596+
597+
这种机制确保了每个数字(从 1 到 9)只会被成功设置并打印一次,并且是按顺序进行的。
520598

521599
![](https://oss.javaguide.cn/github/javaguide/java/basis/unsafe/image-20220717144939826.png)
522600

523-
需要注意的是,在调用`compareAndSwapInt`方法后,会直接返回`true``false`的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在`AtomicInteger`类的设计中,也是采用了将`compareAndSwapInt`的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。
601+
需要注意的是:
602+
603+
1. **自旋逻辑:** `compareAndSwapInt` 方法本身只执行一次比较和交换操作,并立即返回结果。因此,为了确保操作最终成功(在值符合预期的情况下),我们需要在代码中显式地实现自旋逻辑(如 `while(true)` 循环),不断尝试直到 CAS 操作成功。
604+
2. **`AtomicInteger` 的实现:** JDK 中的 `java.util.concurrent.atomic.AtomicInteger` 类内部正是利用了类似的 CAS 操作和自旋逻辑来实现其原子性的 `getAndIncrement()`, `compareAndSet()` 等方法。直接使用 `AtomicInteger` 通常是更安全、更推荐的做法,因为它封装了底层的复杂性。
605+
3. **ABA 问题:** CAS 操作本身存在 ABA 问题(一个值从 A 变为 B,再变回 A,CAS 检查时会认为值没有变过)。在某些场景下,如果值的变化历史很重要,可能需要使用 `AtomicStampedReference` 来解决。但在本例的简单递增场景中,ABA 问题通常不构成影响。
606+
4. **CPU 消耗:** 长时间的自旋会消耗 CPU 资源。在竞争激烈或条件长时间不满足的情况下,可以考虑加入更复杂的退避策略(如 `Thread.sleep()``LockSupport.parkNanos()`)来优化。
524607

525608
### 线程调度
526609

0 commit comments

Comments
 (0)