Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions docs/java/concurrent/aqs.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,150 @@ AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程

一般来说,自定义同步器的共享方式要么是独占,要么是共享,他们也只需实现`tryAcquire-tryRelease`、`tryAcquireShared-tryReleaseShared`中的一种即可。但 AQS 也支持自定义同步器同时实现独占和共享两种方式,如`ReentrantReadWriteLock`。

#### 独占模式与共享模式的深入对比

虽然都是基于 AQS 实现,但独占模式和共享模式在设计理念和实现细节上有着本质的区别。

**独占模式(Exclusive)的特点:**

1. **排他性**:同一时刻只能有一个线程持有资源。当一个线程获取到资源后,其他线程必须等待该线程释放资源才能竞争。
2. **state 语义**:`state` 表示资源的占用状态和重入次数。`state = 0` 表示未被占用,`state > 0` 表示被占用(值代表重入次数)。
3. **实现方法**:需要实现 `tryAcquire()` 和 `tryRelease()` 方法。
4. **典型应用**:`ReentrantLock`、`ReentrantReadWriteLock` 的写锁。

**共享模式(Shared)的特点:**

1. **共享性**:允许多个线程同时持有资源。一个线程获取资源成功后,可能会触发其他等待线程的连锁唤醒。
2. **state 语义**:`state` 表示剩余可用资源数量。比如 `Semaphore` 中,`state` 表示剩余许可证数量;`CountDownLatch` 中,`state` 表示还需要完成的计数。
3. **实现方法**:需要实现 `tryAcquireShared()` 和 `tryReleaseShared()` 方法。返回值有特殊含义:负数表示失败,0 表示成功但无剩余资源,正数表示成功且有剩余资源。
4. **典型应用**:`Semaphore`、`CountDownLatch`、`CyclicBarrier`、`ReentrantReadWriteLock` 的读锁。

**核心区别举例**:

假设有一个停车场,有 10 个车位。

- **独占模式**就像是**单人车位**:一个车位只能停一辆车,其他车必须等这辆车开走才能进来。这就是 `ReentrantLock` 的模型。
- **共享模式**就像是**共享停车场**:10 个车位可以同时停 10 辆车,每辆车进来时检查是否还有空位(`state > 0`),有就停进去并把可用车位数减 1。这就是 `Semaphore` 的模型。

**传播机制的区别**:

这是共享模式最特殊的地方。在共享模式下,当一个线程释放资源后,不仅会唤醒后继节点,被唤醒的节点获取资源成功后,还可能继续唤醒它的后继节点,形成"传播效应"。这就是为什么会有 `PROPAGATE` 状态的原因。

而在独占模式下,资源释放后只会唤醒一个后继节点,不存在连锁唤醒的情况。

**举个例子**:想象一个会议室预订系统。如果是独占模式,一个会议室同一时间只能被一个团队使用。但如果改成共享模式(比如一个大型阶梯教室可以容纳多个小组同时开会),当有一个小组释放了位置,可能会触发多个等待的小组同时进入。第一个小组进入后发现还有空间,就会通知第二个小组也可以进来(传播机制),第二个小组进入后发现还有空间,又会通知第三个小组,形成连锁反应。

### Condition 条件队列的工作机制

Condition 是 AQS 提供的另一个重要功能,它实现了类似 `Object.wait()` 和 `Object.notify()` 的等待/通知机制,但功能更强大也更灵活。

#### Condition 队列与同步队列的关系

AQS 内部实际上维护了**两种队列**:

1. **同步队列(Sync Queue)**:就是前面讲的 CLH 变体队列,用于线程竞争锁。
2. **条件队列(Condition Queue)**:每个 Condition 对象内部维护的单向链表,用于线程等待特定条件。

一个锁可以有多个 Condition 对象,每个 Condition 都有自己独立的条件队列。这比 `synchronized` 的 `wait/notify` 机制灵活得多,后者只有一个等待队列。

#### Condition 的工作流程

**await() 操作的流程:**

1. 当前线程必须先持有锁(否则抛出 `IllegalMonitorStateException`)
2. 将当前线程封装成 Node 节点,加入到 Condition 的条件队列尾部
3. 完全释放锁(即使是重入锁,也要将 state 减到 0)
4. 阻塞当前线程,等待被 `signal` 唤醒
5. 被唤醒后,节点从条件队列移到同步队列,重新竞争锁
6. 获取锁成功后,从 `await()` 方法返回

**signal() 操作的流程:**

1. 当前线程必须先持有锁
2. 从条件队列的头部取出一个节点
3. 将这个节点从条件队列移到同步队列
4. 通过 `unpark` 唤醒该节点对应的线程
5. 被唤醒的线程会在同步队列中竞争锁

**举个生动的例子**:

可以把 Condition 想象成医院的候诊系统:

- **同步队列**就像是**挂号大厅**:所有人都在这里排队等待叫号看病(竞争锁)
- **条件队列**就像是**检查室的等候区**:医生让你去做检查,你从挂号大厅(同步队列)进入检查等候区(条件队列),等待检查结果(await)
- 当检查结果出来(signal),护士会通知你,你再次回到挂号大厅(同步队列)重新排队等待叫号

一个医院可以有多个检查室(多个 Condition 对象),每个检查室都有自己的等候区(独立的条件队列)。这比 `synchronized` 只有一个等候区要灵活得多。

#### Condition 的经典应用场景

**生产者-消费者模型**:

```java
// 使用两个 Condition 分别控制生产和消费
Condition notFull = lock.newCondition(); // 队列未满
Condition notEmpty = lock.newCondition(); // 队列非空

// 生产者
while (队列已满) {
notFull.await(); // 等待队列有空位
}
// 生产数据
notEmpty.signal(); // 通知消费者来消费

// 消费者
while (队列为空) {
notEmpty.await(); // 等待队列有数据
}
// 消费数据
notFull.signal(); // 通知生产者可以生产
```

这种设计比使用 `synchronized` + `wait/notify` 更清晰,避免了"惊群效应"(所有等待线程都被唤醒但只有一个能工作)。

### 公平锁与非公平锁的性能对比

在 AQS 的基础上,可以实现公平锁和非公平锁两种策略,它们在性能和公平性之间做了不同的权衡。

#### 实现机制的区别

**公平锁(Fair Lock)**:
- 严格按照线程到达的顺序分配锁
- 新来的线程总是先检查队列中是否有等待的线程,如果有就乖乖排队
- 实现:在 `tryAcquire()` 中会调用 `hasQueuedPredecessors()` 检查队列中是否有前驱节点

**非公平锁(Nonfair Lock)**:
- 新来的线程会先尝试抢占锁,抢占失败才排队
- 如果锁刚好被释放,新线程可能直接获得锁,而不管队列中是否有等待的线程
- 实现:直接执行 CAS 操作尝试获取锁,不检查队列

#### 性能对比与权衡

根据 Doug Lea(AQS 作者)的测试和实际生产环境的统计:

**吞吐量对比**:
- 在高并发场景下,非公平锁的吞吐量通常是公平锁的 **10-20 倍**
- 原因:减少了线程切换的开销。公平锁每次都要排队,涉及线程的阻塞和唤醒;非公平锁允许"插队",如果锁刚好可用,当前线程可以立即获得,避免了上下文切换

**响应时间对比**:
- 公平锁的响应时间更稳定,不会出现某个线程长时间得不到锁的情况
- 非公平锁的平均响应时间更短,但可能出现"线程饥饿"现象(某些线程长时间得不到锁)

**实际应用建议**:

1. **默认使用非公平锁**:`ReentrantLock` 的无参构造器创建的就是非公平锁,这是因为在大多数场景下,吞吐量比顺序性更重要。
2. **需要严格顺序时使用公平锁**:比如任务调度系统、售票系统等需要保证先来先服务的场景。
3. **阿里巴巴的实践**:在《Java 开发手册》中建议,除非业务场景明确需要公平性,否则应该使用非公平锁以获得更好的性能。

**举个例子**:

想象一个咖啡店的点单台:

- **公平锁模式**:所有顾客必须严格按照到达顺序排队,即使前一个顾客刚点完单走开,收银员在处理订单,新来的顾客也必须等前面所有人都轮到后才能点单。
- **非公平锁模式**:如果前一个顾客刚点完单,收银员正好空闲,新来的顾客可以直接上前点单,不用管后面是否还有排队的人。虽然可能对排队的人不公平,但整体效率更高,顾客等待时间的平均值更短。

在大型互联网公司的实践中(如阿里、美团),非公平锁因为性能优势被广泛采用。但在金融系统、抢票系统等需要保证公平性的场景,会选择使用公平锁,即使牺牲一些性能。

### AQS 资源获取源码分析(独占模式)

AQS 中以独占模式获取资源的入口方法是 `acquire()` ,如下:
Expand Down
80 changes: 80 additions & 0 deletions docs/java/concurrent/cas.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,86 @@ public boolean compareAndSet(V expectedReference,
}
```

#### ABA 问题的实际影响场景

虽然 ABA 问题在理论上存在,但在实际应用中是否真的会造成影响需要具体分析:

**无影响场景**: 如果只关心变量的当前值而不关心变更历史,ABA 问题通常不会造成实际影响。例如 `AtomicInteger` 的累加操作,即使值经历了 A→B→A 的变化,最终累加结果仍然正确。

**有影响场景**: 在需要维护数据结构完整性的场景中,ABA 问题可能导致严重错误。典型案例是**无锁栈**的出栈操作:

```
线程 1 准备 CAS 弹出节点 A (A→B→C)
线程 2 弹出 A 和 B,然后重新压入 A (栈变为 A→C)
线程 1 的 CAS 成功(发现栈顶仍是 A),将 next 指向原来的 B
结果: 节点 C 丢失,B 可能已被回收导致悬挂指针
```

因此,涉及指针操作和数据结构维护时,应使用 `AtomicStampedReference` 或 `AtomicMarkableReference` 避免 ABA 问题。

### CAS 的 CPU 层面实现

在 x86 架构下,CAS 操作最终会被编译为 **CMPXCHG 指令**,并配合 **LOCK 前缀**保证原子性:

```assembly
LOCK CMPXCHG [内存地址], 新值
```

**LOCK 前缀的作用:**

1. **锁定总线或缓存行**: 在多核处理器中,LOCK 前缀会锁定相关的缓存行(而非整个总线),防止其他 CPU 同时修改该内存位置
2. **保证内存可见性**: 强制将写缓冲区的数据刷新到主内存,并使其他 CPU 的缓存行失效

这就是为什么 CAS 能够在硬件层面保证原子性的原因。相比 synchronized 的重量级锁,CAS 仅锁定单个缓存行,因此开销小得多。

### 自旋与阻塞的性能权衡

CAS 失败时会进行自旋重试,这在不同竞争程度下有不同的性能表现:

**低竞争场景**: CAS 几乎不会失败,自旋开销极小,性能远超加锁方式(避免了线程上下文切换)。

**高竞争场景**: CAS 频繁失败导致大量自旋,CPU 空转消耗资源。此时 synchronized 或 Lock 的阻塞机制反而更优,因为线程会被挂起释放 CPU。

JDK 的 **自适应自旋锁** 就是基于这种权衡:JVM 会根据历史竞争情况动态调整自旋次数,在高竞争时及时转为阻塞。

### LongAdder 的分段 CAS 优化

在高并发计数场景下,多个线程对同一个 `AtomicLong` 进行 CAS 更新会导致激烈竞争。`LongAdder` 通过**分段计数**大幅降低竞争:

**核心思想**: 维护一个 `Cell` 数组,每个线程优先更新自己的 Cell,最终求和获取总值。

```java
// LongAdder 内部结构
transient volatile Cell[] cells;
transient volatile long base;

final void longAccumulate(long x, ...) {
// 如果 cells 为空,先尝试更新 base
if (cells == null) {
if (casBase(b = base, b + x))
return;
}
// 否则找到线程对应的 Cell 进行更新
Cell c = cells[getProbe() & m];
if (c.cas(v = c.value, v + x))
return;
// 更新失败则扩容或rehash
}

public long sum() {
Cell[] cs = cells;
long sum = base;
if (cs != null) {
for (Cell c : cs)
if (c != null)
sum += c.value;
}
return sum;
}
```

这种设计与 `ConcurrentHashMap` 的 `counterCells` 机制完全一致,都是通过**分散热点、化整为零**的思想实现高并发下的性能优化。在累加场景下,`LongAdder` 的性能可比 `AtomicLong` 高出数倍。

### 循环时间长开销大

CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。
Expand Down
Loading