mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-16 18:10:13 +08:00
fix: cas 示例代码修正
This commit is contained in:
parent
dbdd3aaedd
commit
b1867d3f44
@ -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 要做这个改变呢?主要有两大原因:**
|
||||
|
||||
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 握手:
|
||||
|
||||
|
@ -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)只会被成功设置并打印一次,并且是按顺序进行的。
|
||||
|
||||

|
||||
|
||||
需要注意的是,在调用`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()`)来优化。
|
||||
|
||||
### 线程调度
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user