255 lines
9.6 KiB
Markdown
255 lines
9.6 KiB
Markdown
|
|
---
|
|||
|
|
tags: [java/lang, lock-escalation, CAS, AQS, optimistic-lock]
|
|||
|
|
create time: 2026-08-08 18:00
|
|||
|
|
update time: 2026-08-08 18:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 锁升级与 CAS 机制
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
Java 的 synchronized 关键字从 JDK 1.5 到 JDK 1.6 经历了一次重大升级——引入锁升级机制,让锁在低竞争时以极轻量方式运行,在高竞争时平滑过渡到重量级互斥锁。这一设计的核心是 **CAS(Compare And Swap)** 原子操作和 **AQS(AbstractQueuedSynchronizer)** 框架,它们共同构成了 Java 并发包的底层基石。
|
|||
|
|
|
|||
|
|
## 核心原理
|
|||
|
|
|
|||
|
|
### 锁升级之路:偏向 → 轻量级 → 重量级
|
|||
|
|
|
|||
|
|
synchronized 的锁状态不是静态的,它会随着竞争程度逐步"膨胀":
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
stateDiagram-v2
|
|||
|
|
[*] --> 无锁状态
|
|||
|
|
无锁状态 --> 偏向锁: 第一个线程获取锁
|
|||
|
|
偏向锁 --> 偏向锁: 同一线程重复获取\n(时间戳比较)
|
|||
|
|
偏向锁 --> 轻量级锁: 其他线程尝试获取\n(发生竞争)
|
|||
|
|
轻量级锁 --> 轻量级锁: CAS自旋成功\n(少数竞争者)
|
|||
|
|
轻量级锁 --> 重量级锁: CAS失败次数过多\n或自旋超时
|
|||
|
|
重量级锁 --> 轻量级锁: 竞争减弱\n(Monitor退出队列)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 第一阶段:偏向锁(Biased Locking)
|
|||
|
|
|
|||
|
|
**触发条件**:第一个线程进入同步块时,JVM 会在对象头中记录当前线程 ID,后续该线程再次进入时无需任何同步操作。
|
|||
|
|
|
|||
|
|
- JDK 6 默认启用,需要 `-XX:+UseBiasedLocking`。
|
|||
|
|
- JDK 15 中被标记为废弃,JDK 17 中被移除。因为实际场景中"永远只有一个人访问"的概率很低,而偏向锁的撤销代价较高(需全局 STW 撤销所有偏向)。
|
|||
|
|
|
|||
|
|
**对象头结构(HotSpot,64位)**:
|
|||
|
|
|
|||
|
|
| 偏移 | 字段 | 大小 |
|
|||
|
|
|------|------|------|
|
|||
|
|
| 0-2 bit | Mark Word 低 3 位 | 锁标志位 |
|
|||
|
|
| 3-31 bit | 偏向锁标记 + ThreadID | 优先权 + 线程 ID |
|
|||
|
|
| 32-63 bit | 分代年龄 + hashCode | 64 位扩展信息 |
|
|||
|
|
|
|||
|
|
当有其他线程尝试获取偏向锁时,JVM 会撤销该对象的偏向状态,升级为轻量级锁。
|
|||
|
|
|
|||
|
|
#### 第二阶段:轻量级锁(Lightweight Locking)
|
|||
|
|
|
|||
|
|
**触发条件**:偏向锁被剥夺后,或者从一开始就存在多个线程竞争。
|
|||
|
|
|
|||
|
|
核心机制:**利用 CAS 替换对象头中的 Mark Word**。
|
|||
|
|
|
|||
|
|
流程:
|
|||
|
|
1. 线程在栈帧中创建 **Lock Record**,复制对象头的 Mark Word 到其中。
|
|||
|
|
2. 使用 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针。
|
|||
|
|
3. 如果 CAS 成功,当前线程获得锁;如果失败,说明有竞争。
|
|||
|
|
4. 竞争发生时,当前线程自旋重试(最多循环指定次数)。
|
|||
|
|
5. 如果自旋超过阈值(默认 10 次,可通过 `-XX:PreBlockSpin` 调整),升级为重量级锁。
|
|||
|
|
|
|||
|
|
> [!NOTE]
|
|||
|
|
> "轻量级"并不意味着不需要操作系统内核帮助——只是在没有竞争时使用用户态 CAS,避免了上下文切换开销。一旦竞争激烈,它最终会退化为重量级锁。
|
|||
|
|
|
|||
|
|
#### 第三阶段:重量级锁(Heavyweight Locking)
|
|||
|
|
|
|||
|
|
**触发条件**:轻量级锁的 CAS 和自旋都未能成功获取锁。
|
|||
|
|
|
|||
|
|
此时 Monitor 对象成为真正的互斥锁:
|
|||
|
|
- 未获得锁的线程会被阻塞挂起(进入 OS 的等待队列)。
|
|||
|
|
- 阻塞/唤醒操作需要切换到内核态,这是性能损耗最大的环节。
|
|||
|
|
|
|||
|
|
### Unsafe 类与 CAS 原理
|
|||
|
|
|
|||
|
|
`Unsafe` 是 JVM 提供的一个"后门"类,允许 Java 代码直接操作内存和执行原子操作。其中的 CAS 原语是所有乐观锁的基础。
|
|||
|
|
|
|||
|
|
**核心方法签名**:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
|
|||
|
|
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
|
|||
|
|
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`offset` 是通过 `objectFieldOffset(Field)` 计算出的字段在对象内存布局中的字节偏移量。
|
|||
|
|
|
|||
|
|
**原理**:CAS 是 CPU 级别的原子指令(x86 下对应 `cmpxchg` 汇编),在多核环境下通过总线锁或缓存锁保证原子性。JVM 内部通过 `Atomic*` 类封装了这些操作,开发者无需直接使用 Unsafe。
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// AtomicInteger.incrementAndGet 的核心逻辑(简化版)
|
|||
|
|
private volatile int value;
|
|||
|
|
public final int incrementAndGet() {
|
|||
|
|
int prev, next;
|
|||
|
|
do {
|
|||
|
|
prev = get(); // 读取当前值
|
|||
|
|
next = prev + 1; // 计算新值
|
|||
|
|
} while (!compareAndSet(prev, next)); // CAS 更新
|
|||
|
|
return next;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这段代码用 12 行实现了线程安全的自增——没有使用任何 synchronized。其关键保障来自 `while` 循环:如果 CAS 失败(说明中间有其他线程修改了 value),就重新读取、重新计算、重新尝试,直到成功。
|
|||
|
|
|
|||
|
|
> [!WARNING]
|
|||
|
|
> CAS 的三个问题:
|
|||
|
|
> 1. ABA 问题(见下文)
|
|||
|
|
> 2. 只能保证一个共享变量的原子操作,无法做多变量联合更新
|
|||
|
|
> 3. 长时间自旋会增加 CPU 开销——这就是为什么有界锁最终会升级为重量级锁
|
|||
|
|
|
|||
|
|
#### ABA 问题与 AtomicStampedReference
|
|||
|
|
|
|||
|
|
ABA 问题是 CAS 的经典缺陷:线程 T1 读取值为 A,另一个线程 T2 把值改为 B 再改回 A,T1 的 CAS 检查时发现值仍是 A,误以为没有被修改过。
|
|||
|
|
|
|||
|
|
解决思路:**给值加版本号**——每次变更版本号加 1。即使值回到 A,版本号也已不同。
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 伪代码示意 AtomicStampedReference 用法
|
|||
|
|
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
|
|||
|
|
int[] stampHolder = new int[1];
|
|||
|
|
String current = ref.get(stampHolder); // 返回 ["A", 0]
|
|||
|
|
int stamp = stampHolder[0];
|
|||
|
|
|
|||
|
|
// CAS 时需要同时匹配值和版本号
|
|||
|
|
ref.compareAndSet("A", "B", stamp, stamp + 1); // [A, 0] -> [B, 1]
|
|||
|
|
ref.compareAndSet("B", "A", stamp + 1, stamp + 2); // [B, 1] -> [A, 2]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### AQS(AbstractQueuedSynchronizer)核心设计
|
|||
|
|
|
|||
|
|
AQS 是 `java.util.concurrent` 包的基石——ReentrantLock、CountDownLatch、CyclicBarrier、Semaphore 全部基于它构建。
|
|||
|
|
|
|||
|
|
#### CLH 队列模型
|
|||
|
|
|
|||
|
|
AQS 维护了一个 FIFO 的等待队列(CLH 变体),每个节点代表一个等待资源的线程:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
flowchart LR
|
|||
|
|
H["head"] --> N1["Node 1<br/>WAITING"]
|
|||
|
|
N1 --> N2["Node 2<br/>SIGNAL"]
|
|||
|
|
N2 --> N3["Node 3<br">CONDITION"]
|
|||
|
|
N3 --> NULL["null (tail)"]
|
|||
|
|
|
|||
|
|
style H stroke-dasharray: 5 5
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
核心规则:
|
|||
|
|
- **头节点(head)** 是当前持有锁的节点,它的线程正在执行。
|
|||
|
|
- **尾节点(tail)** 是新入队节点的插入位置。
|
|||
|
|
- 前驱节点释放锁时会唤醒后继节点(通过 `park()` → `unpark()`)。
|
|||
|
|
- 节点状态包括:`CANCELLED`(已取消)、`SIGNAL`(后继需 unpark)、`CONDITION`(等待条件变量)、`PROPAGATE`(共享模式传播)。
|
|||
|
|
|
|||
|
|
#### state 状态机
|
|||
|
|
|
|||
|
|
AQS 的核心是一个 `volatile int state` 变量,表示同步状态:
|
|||
|
|
|
|||
|
|
| 同步器 | state 含义 |
|
|||
|
|
|--------|-----------|
|
|||
|
|
| ReentrantLock | 持有锁的次数(重入计数) |
|
|||
|
|
| CountDownLatch | 计数器初始值,递减到 0 触发 |
|
|||
|
|
| Semaphore | 可用许可证数量 |
|
|||
|
|
| ReentrantReadWriteLock | 高 16 位读计数,低 16 位写计数 |
|
|||
|
|
|
|||
|
|
#### tryAcquire / tryRelease 模板方法
|
|||
|
|
|
|||
|
|
子类只需实现这两个抽象方法,AQS 处理所有队列管理细节:
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// ReentrantLock 的非公平锁 tryAcquire 核心逻辑(简化)
|
|||
|
|
protected final boolean tryAcquire(int acquires) {
|
|||
|
|
Thread current = Thread.currentThread();
|
|||
|
|
int c = getState();
|
|||
|
|
if (c == 0) {
|
|||
|
|
// 无竞争时直接用 CAS 拿锁
|
|||
|
|
if (compareAndSetState(0, acquires)) {
|
|||
|
|
setExclusiveOwnerThread(current);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
} else if (current == getExclusiveOwnerThread()) {
|
|||
|
|
// 重入:累加 state
|
|||
|
|
setState(c + acquires);
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
return false; // 竞争发生,走 AQS 排队流程
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!TIP]
|
|||
|
|
> 面试常考点:AQS 的 `setState()` 用了 `volatile` 但非 CAS——因为重入场景下只增加不减少,且由独占线程自己操作,不存在多线程竞写问题。
|
|||
|
|
|
|||
|
|
### 三大常用工具类的 AQS 应用
|
|||
|
|
|
|||
|
|
#### CountDownLatch
|
|||
|
|
|
|||
|
|
单向计数器,减到 0 后释放所有等待线程。**不可重置**,适合"等齐事件"场景。
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 主线程等待 5 个初始化任务完成
|
|||
|
|
CountDownLatch latch = new CountDownLatch(5);
|
|||
|
|
for (int i = 0; i < 5; i++) {
|
|||
|
|
executor.submit(() -> {
|
|||
|
|
doInit();
|
|||
|
|
latch.countDown(); // 每完成一个减 1
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
latch.await(); // 阻塞直到计数器归零
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### CyclicBarrier
|
|||
|
|
|
|||
|
|
可循环使用的栅栏,到达指定数量后统一放行。**可以复用**,适合多阶段并行计算。
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 三组数据并行处理,完成后合并结果
|
|||
|
|
CyclicBarrier barrier = new CyclicBarrier(3, resultMerger::merge);
|
|||
|
|
for (DataSource ds : dataSources) {
|
|||
|
|
executor.submit(() -> {
|
|||
|
|
Result r = ds.process();
|
|||
|
|
barrier.await(); // 等待同伴
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### Semaphore
|
|||
|
|
|
|||
|
|
控制并发访问的资源信号量。常用于限流——限制同时运行的任务数。
|
|||
|
|
|
|||
|
|
```java
|
|||
|
|
// 限制同时执行 10 个 IO 请求
|
|||
|
|
Semaphore sem = new Semaphore(10);
|
|||
|
|
executor.submit(() -> {
|
|||
|
|
sem.acquire(); // 拿令牌,不足则阻塞
|
|||
|
|
try {
|
|||
|
|
ioRequest.send();
|
|||
|
|
} finally {
|
|||
|
|
sem.release(); // 归还令牌
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 实践场景
|
|||
|
|
|
|||
|
|
**面试高频对比题**:
|
|||
|
|
|
|||
|
|
| 维度 | synchronized | ReentrantLock |
|
|||
|
|
|------|-------------|---------------|
|
|||
|
|
| 实现层级 | JVM 内置关键字 | JDK API 层 |
|
|||
|
|
| 锁升级 | 偏向→轻量级→重量级 | 无(始终 AQS + CAS) |
|
|||
|
|
| 公平性 | 非公平 | 可选公平/非公平 |
|
|||
|
|
| 中断响应 | 不响应中断 | `lockInterruptibly()` 支持 |
|
|||
|
|
| 条件变量 | wait()/notify() | 多个 Condition |
|
|||
|
|
| 超时获取 | 不支持 | `tryLock(timeout)` |
|
|||
|
|
| 可重入 | 是 | 是 |
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[01.Java/concurrent/线程池参数与拒绝策略]]
|