9.6 KiB
tags, create time, update time
| tags | create time | update time | |||||
|---|---|---|---|---|---|---|---|
|
2026-08-08 18:00 | 2026-08-08 18:00 |
锁升级与 CAS 机制
概述
Java 的 synchronized 关键字从 JDK 1.5 到 JDK 1.6 经历了一次重大升级——引入锁升级机制,让锁在低竞争时以极轻量方式运行,在高竞争时平滑过渡到重量级互斥锁。这一设计的核心是 CAS(Compare And Swap) 原子操作和 AQS(AbstractQueuedSynchronizer) 框架,它们共同构成了 Java 并发包的底层基石。
核心原理
锁升级之路:偏向 → 轻量级 → 重量级
synchronized 的锁状态不是静态的,它会随着竞争程度逐步"膨胀":
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。
流程:
- 线程在栈帧中创建 Lock Record,复制对象头的 Mark Word 到其中。
- 使用 CAS 将对象头的 Mark Word 替换为指向 Lock Record 的指针。
- 如果 CAS 成功,当前线程获得锁;如果失败,说明有竞争。
- 竞争发生时,当前线程自旋重试(最多循环指定次数)。
- 如果自旋超过阈值(默认 10 次,可通过
-XX:PreBlockSpin调整),升级为重量级锁。
Note
"轻量级"并不意味着不需要操作系统内核帮助——只是在没有竞争时使用用户态 CAS,避免了上下文切换开销。一旦竞争激烈,它最终会退化为重量级锁。
第三阶段:重量级锁(Heavyweight Locking)
触发条件:轻量级锁的 CAS 和自旋都未能成功获取锁。
此时 Monitor 对象成为真正的互斥锁:
- 未获得锁的线程会被阻塞挂起(进入 OS 的等待队列)。
- 阻塞/唤醒操作需要切换到内核态,这是性能损耗最大的环节。
Unsafe 类与 CAS 原理
Unsafe 是 JVM 提供的一个"后门"类,允许 Java 代码直接操作内存和执行原子操作。其中的 CAS 原语是所有乐观锁的基础。
核心方法签名:
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。
// 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 的三个问题:
- ABA 问题(见下文)
- 只能保证一个共享变量的原子操作,无法做多变量联合更新
- 长时间自旋会增加 CPU 开销——这就是为什么有界锁最终会升级为重量级锁
ABA 问题与 AtomicStampedReference
ABA 问题是 CAS 的经典缺陷:线程 T1 读取值为 A,另一个线程 T2 把值改为 B 再改回 A,T1 的 CAS 检查时发现值仍是 A,误以为没有被修改过。
解决思路:给值加版本号——每次变更版本号加 1。即使值回到 A,版本号也已不同。
// 伪代码示意 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 变体),每个节点代表一个等待资源的线程:
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 处理所有队列管理细节:
// 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 后释放所有等待线程。不可重置,适合"等齐事件"场景。
// 主线程等待 5 个初始化任务完成
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
executor.submit(() -> {
doInit();
latch.countDown(); // 每完成一个减 1
});
}
latch.await(); // 阻塞直到计数器归零
CyclicBarrier
可循环使用的栅栏,到达指定数量后统一放行。可以复用,适合多阶段并行计算。
// 三组数据并行处理,完成后合并结果
CyclicBarrier barrier = new CyclicBarrier(3, resultMerger::merge);
for (DataSource ds : dataSources) {
executor.submit(() -> {
Result r = ds.process();
barrier.await(); // 等待同伴
});
}
Semaphore
控制并发访问的资源信号量。常用于限流——限制同时运行的任务数。
// 限制同时执行 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) |
| 可重入 | 是 | 是 |