面试里最经典的一串追问大概是这样的:synchronized 的实现原理是什么?它是 AQS 实现的吗?那 ReentrantLock 呢?CountDownLatch 又是怎么工作的?
这四个问题其实是同一条链路:从 JVM 内置锁,到 J.U.C 类库的骨架 AQS,再到 AQS 的两种使用模式。这篇文章就按这条链路自底向上走一遍,把每个环节的底层机制讲透。
一、synchronized:JVM 层的原生实现
synchronized 的实现可以分三层来看:字节码层、对象头层、JVM 运行时层。
1.1 字节码层:monitorenter / monitorexit
编译后,同步代码块被编译成 monitorenter + monitorexit 指令(同步方法则是 ACC_SYNCHRONIZED 标志位,JVM 在调用时隐式加锁)。每个对象关联一个 Monitor(监视器),线程执行 monitorenter 时尝试获取它。
1.2 对象头:Mark Word
锁信息记录在对象头的 Mark Word 里(64 位 JVM):
| 锁状态 | 存储内容 | 标志位 |
|---|---|---|
| 无锁 | hashCode + 分代年龄 | 01 |
| 偏向锁 | 线程 ID + epoch + 分代年龄 | 01 |
| 轻量级锁 | 指向栈中 Lock Record 的指针 | 00 |
| 重量级锁 | 指向 ObjectMonitor 的指针 | 10 |
同一块内存,随锁状态变化而复用,这就是锁升级的基础。
1.3 运行时层:锁升级
HotSpot 中 Monitor 由 C++ 的 ObjectMonitor 实现,直接依赖操作系统 mutex,获取失败会阻塞线程并发生用户态到内核态的切换,代价很高。因此 JDK 6 引入了自适应锁:
flowchart LR
A["无锁"] -->|"首次访问"| B["偏向锁:Mark Word 记录线程 ID"]
B -->|"出现第二个线程竞争"| C["轻量级锁:CAS + 自旋"]
C -->|"自旋失败或竞争加剧"| D["重量级锁:ObjectMonitor 阻塞"]
锁只升不降,每一级的设计目标都是:竞争越轻,开销越小。
偏向锁:只有一个线程反复获取时,Mark Word 直接记下线程 ID,之后该线程进入无需任何 CAS。有第二个线程来竞争才升级。需要注意,JDK 15 起偏向锁已默认废弃(JEP 374),维护成本高、现代场景收益小是主因。
轻量级锁:线程在自己的栈帧里创建 Lock Record,用 CAS 把 Mark Word 替换成指向它的指针。适合线程交替执行、竞争轻微的场景;CAS 自旋失败(或自旋超时)后升级。自旋次数由 JVM 根据该锁上一次自旋是否成功动态调整,即自适应自旋。
重量级锁:Mark Word 指向 ObjectMonitor,核心字段如下:
ObjectMonitor {
_owner // 持有线程
_EntryList // 阻塞在 monitorenter 上的线程队列
_WaitSet // 调用 wait() 的线程队列
_recursions // 重入计数(synchronized 是可重入的)
...
}
竞争失败的线程进入 _EntryList 并被挂起,锁释放时按策略唤醒。
1.4 其他优化与语义保证
JIT 还有两招:
- 锁消除(Lock Elision):逃逸分析判定锁对象不会逃出方法,直接删掉锁。典型如方法内局部
StringBuffer的同步。 - 锁粗化(Lock Coarsening):循环里反复加解锁时,JIT 把锁范围扩大到循环外。
语义上,synchronized 保证三件事:互斥(同一时刻一个线程进入临界区)、可见性(JMM 规定 unlock happens-before 后续对同一把锁的 lock)、可重入(通过 _recursions 计数实现)。wait/notify 必须在持有锁时调用,因为它们操作的就是 ObjectMonitor 的 _WaitSet。
小结:synchronized 的本质是字节码指令 + 对象头 Mark Word + HotSpot 的 ObjectMonitor。JDK 6 之后它不再是纯粹的”重量级锁”,无竞争场景下几乎零成本,这也是”能用 synchronized 就先用它”的理由。
二、先澄清一个误区:synchronized 不是 AQS 实现的
这是非常常见的混淆点。synchronized 和 AQS 是两套完全独立的实现:
synchronized由 JVM 用 C++ 直接实现,走monitorenter/monitorexit、Mark Word、ObjectMonitor,没有用到任何 Java 类库——逻辑上 AQS 运行时还要依赖 JVM,AQS 在它之上;- AQS(
AbstractQueuedSynchronizer)是 JDK 1.5 时 Doug Lea 写的java.util.concurrent基类,是纯 Java 实现; ObjectMonitor里的_EntryList/_WaitSet和 AQS 的 CLH 队列只是思想相似(都是把竞争失败的线程排队管理),但一个是 C++ 数据结构,一个是 Java 类。
混淆的来源也好理解:ReentrantLock 和 synchronized 都是可重入互斥锁,面试资料经常并列对比;两者原理又都遵循”一个状态 + 一个等待队列”的骨架,不少文章就混着讲了。
一句话记住对应关系:synchronized 是 JVM 级(内建锁),ReentrantLock 是 API 级(AQS)。
三、AQS:整个 J.U.C 的骨架
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock、线程池里的 Worker,全都是 AQS 的子类。理解 AQS 的两个核心组件,后面的内容就都是应用题。
3.1 state:同步状态
// AQS 源码
private volatile int state; // 语义由子类定义
private transient Thread exclusiveOwnerThread; // 当前持锁线程(父类字段)
state 是一个 volatile int,语义完全由子类定义:在 ReentrantLock 里是重入计数,在 CountDownLatch 里是剩余计数,在 Semaphore 里是剩余许可数。
3.2 CLH 变体队列
static final class Node {
volatile int waitStatus; // CANCELLED / SIGNAL / CONDITION / 0
volatile Node prev;
volatile Node next;
volatile Thread thread;
...
}
一个虚拟 head 节点 + 双向链表,竞争失败的线程包装成 Node 入队,用 LockSupport.park() 挂起(底层经 Unsafe.park() 走到 JVM 的原生阻塞)。
3.3 模板方法模式
AQS 的设计意图是:模板方法管流程,子类管语义。外层的 acquire/release/acquireShared/releaseShared 负责排队、挂起、唤醒这些复杂逻辑;子类只需要实现几个小方法来定义”什么是获取成功”。独占模式的核心方法返回 boolean,共享模式的返回 int 三态——这个差异正是后两节的主角。
四、ReentrantLock:AQS 的独占模式
ReentrantLock 本身只是个壳,所有工作委托给内部的 Sync(继承 AQS),NonfairSync 和 FairSync 两个子类分别实现非公平与公平语义。
4.1 加锁流程(非公平为例)
// NonfairSync
final void lock() {
if (compareAndSetState(0, 1)) // ① 上来直接抢
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // ② 抢不到走完整流程
}
acquire 是 AQS 的模板方法:
public final void acquire(int arg) {
if (!tryAcquire(arg) && // ① 尝试拿锁
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // ② 失败则入队
selfInterrupt(); // ③ 补上中断标记
}
4.2 tryAcquire:可重入与公平性的全部秘密
// 非公平版本
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 不看队列,直接抢
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 重入:state 加 1
setState(c + acquires);
return true;
}
return false;
}
两个关键点:
- 可重入的实现就是
current == getExclusiveOwnerThread()分支:不换持有者,只把 state 加 1; - 公平 vs 非公平的全部区别在于 state 为 0 时是否先检查
hasQueuedPredecessors()——公平锁要求队列里没人排队才允许 CAS,非公平锁直接抢。公平锁因此多一次队列检查,吞吐更低,但换来无饥饿的 FIFO 语义。
非公平吞吐高的原因:持锁线程释放到唤醒后继之间有时间窗口,这期间新到达的线程若直接拿到锁,就省掉一次线程挂起/唤醒的上下文切换。
4.3 入队与挂起
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 队头且抢到,出队
setHead(node);
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // LockSupport.park() 挂起
interrupted = true;
}
} finally { ... }
}
要点有两个:只有前驱是 head 的节点才有资格抢锁,这保证了 FIFO 唤醒顺序;挂起前必须把前驱的 waitStatus 置为 SIGNAL(意思是”我睡了,你释放时记得唤醒我”),这解决了”唤醒信号先于挂起发生而丢失”的竞态。
4.4 解锁
// ReentrantLock.Sync.tryRelease — 重入减到 0 才真正释放
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 完全释放
free = true;
setExclusiveOwnerThread(null);
}
setState(c); // 只有持锁线程能走到这,无需 CAS
return free;
}
注意 tryRelease 不用 CAS:能走到这里的一定是持锁线程,不存在竞争。state 是 volatile,保证解锁对其他线程可见。外层 release 在完全释放后调用 unparkSuccessor 唤醒后继线程,被唤醒的线程从 park 处醒来回到自旋,重新 tryAcquire。
另外 Condition(await/signal,对应 synchronized 的 wait/notify)也是 AQS 提供的:每个 Condition 有独立等待队列,signal 只是把节点从 Condition 队列转移到同步队列并唤醒,被唤醒线程还得重新抢锁。ArrayBlockingQueue 的阻塞语义就是靠两个 Condition 实现的。
一句话总结:ReentrantLock = AQS 独占模式 + state 语义为重入计数。
五、CountDownLatch:AQS 的共享模式
CountDownLatch 和 ReentrantLock 用的是同一个 AQS,但走共享模式(Shared Mode)。先建立框架感:
| 类 | AQS 模式 | state 的语义 | 唤醒策略 |
|---|---|---|---|
| ReentrantLock | 独占 | 重入计数 | 唤醒单个后继 |
| CountDownLatch | 共享 | 剩余计数(只减不增) | 归零时级联唤醒全部 |
| Semaphore | 共享 | 剩余许可数(可增可减) | 按许可数决定传播 |
5.1 结构:state 就是倒计时
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count); // 构造时的 count 直接存进 state
}
...
}
每 countDown() 一次 state 减 1,减到 0 时”闸门打开”,所有等待线程放行。
5.2 tryAcquireShared 的三态语义
共享模式的核心在于 tryAcquireShared 返回的不是 boolean 而是 int:
- 负数:获取失败,入队挂起;
- 0:获取成功,但不再传播唤醒;
- 正数:获取成功,且唤醒后继共享节点继续传播。
CountDownLatch.Sync 的实现简单粗暴:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
不看参数,只看 state 是否为 0。也就是说 await() 不消费任何东西,它只是一次”state 等于 0 了吗”的检查,不满足就排队睡觉。返回 1(正数)就是”放行信号沿队列传播下去”的燃料。
5.3 countDown:并发的 CAS 减法
protected final boolean tryReleaseShared(int releases) {
for (;;) { // 自旋 CAS:多线程并发调用,必须 CAS
int c = getState();
if (c == 0)
return false; // 已减到 0,重复 countDown 无害
int nextc = c - 1;
if (compareAndSetState(c, nextc))
return nextc == 0; // 只有减到 0 的那一次触发唤醒
}
}
和 ReentrantLock 的 tryRelease 对比一下:独占模式不需要 CAS(只有持锁线程能进来),共享模式的释放是多个线程并发调用的——100 个线程同时 countDown(),必须自旋 CAS 保证减法不丢。
5.4 级联唤醒:setHeadAndPropagate
这是共享模式最精妙的部分。独占模式唤醒后继只唤醒一个;共享模式一旦放行,要唤醒一串——所有等在 await() 上的线程都应该通过。被唤醒的线程抢到锁后执行:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 || ...) {
Node s = node.next;
if (s == null || s.isShared())
unparkSuccessor(s); // 唤醒下一个共享节点
}
}
效果是一条唤醒链:state 减到 0 → 计数线程 unpark 队头 → 队头醒来后 tryAcquireShared 返回 1 → 自己出队的同时 unpark 下一个 → 下一个醒来再 unpark 下下一个 → 所有 await 线程被依次级联唤醒。
完整时序:
sequenceDiagram
participant A as Thread A
participant B as Thread B
participant Q as AQS 同步队列
participant C as Thread C 计数方
A->>Q: await,tryAcquireShared 返回 -1,state=3
A->>Q: 入队并 park 挂起
B->>Q: await,同样挂起
C->>C: countDown,CAS 3 到 2,返回 false
C->>C: countDown,CAS 2 到 1,返回 false
C->>C: countDown,CAS 1 到 0,返回 true
C->>Q: doReleaseShared,unpark 队头等待者 A
A->>A: 醒来,tryAcquireShared 返回 1,成功且传播
A->>Q: setHeadAndPropagate,unpark B
B->>B: 醒来,同样通过,从 await 返回
顺带一提,Semaphore 是同一个机制的反向应用:tryAcquireShared 返回剩余许可数,减到 0 返回 0,传播自然停止——同一个共享框架,两种行为,只由返回值决定。
一句话总结:CountDownLatch = AQS 共享模式 + state 作为倒计时计数器。另外它是一次性的,归零后永久打开;需要重置请用 CyclicBarrier(它不走 AQS 队列,靠 ReentrantLock + Condition 实现)。
六、三者对比与选型
先明确一个前提:这三者不完全是同一类工具。synchronized 和 ReentrantLock 是互斥锁,CountDownLatch 是线程协作工具——它不保护临界区,只协调”什么时候一起走”。
| 维度 | synchronized | ReentrantLock | CountDownLatch |
|---|---|---|---|
| 实现层 | JVM 内置(C++) | 纯 Java(AQS) | 纯 Java(AQS) |
| 用途 | 互斥同步 | 互斥同步 | 一次性倒计时协调 |
| 可重入 | 是 | 是 | 否(没有持有者概念) |
| 公平性 | 非公平,不可选 | 可选 | 不适用 |
| 可中断 | 否 | 是(lockInterruptibly) | 是(await 可中断) |
| 超时获取 | 否 | 是(tryLock(timeout)) | 是(await(timeout)) |
| 条件队列 | 单个(wait/notify) | 多个(Condition) | 不适用 |
| 释放方 | 持锁线程 | 持锁线程 | 任意线程 |
| 复用性 | 随时加解锁 | 随时加解锁 | 一次性 |
| 释放方式 | 块结束自动释放 | 必须 finally 手动 unlock | 无解锁概念 |
三者的共同骨架都是”一个同步状态 + 一个等待队列”,差异在状态语义和放行策略:ReentrantLock 的 state 是排他的,唤醒时只放行一个后继;CountDownLatch 的 state 是公共倒计时,谁都能减,归零时级联放行全部;synchronized 和 AQS 没有代码关系,但 ObjectMonitor 的 _EntryList/_WaitSet 和 AQS 的同步队列/Condition 队列是结构上的同构。
性能与选型
性能上有个需要更新的认知:JDK 6 锁优化之后,synchronized 和 ReentrantLock 在大多数场景差距已经很小——无竞争时 synchronized 甚至更快(偏向/轻量级锁只需一次 CAS 甚至零开销),高竞争时两者都会退化到内核阻塞,量级相当。“ReentrantLock 更快”是 JDK 5 时代的过时结论,现在选型基本不看性能。
选型可以走一棵简单的决策树:
- 要保护共享数据(互斥):默认
synchronized(简单、不会忘解锁);只在需要可中断、超时尝试、公平性或多条件队列时上ReentrantLock(记得 finally unlock); - 要等条件满足再继续(协调):一次性汇合用
CountDownLatch,可复用换CyclicBarrier/Phaser。
注意 CountDownLatch 的一个特点:countDown 和 await 的角色可以完全分离——计数的线程可以不等,等待的线程也可以不计数。这是锁语义覆盖不了的能力。
写在最后
回头看这条链路,其实只有两句话:
synchronized是 JVM 层的”状态 + 等待队列”(Mark Word + ObjectMonitor),AQS 是类库层的”状态 + 等待队列”(state + CLH 队列)——同一种思想,两个层次;- AQS 之上,ReentrantLock 用独占模式(boolean 语义、唤醒单个),CountDownLatch 用共享模式(int 三态、级联传播)——同一个框架,子类只定义语义,流程全部复用。
把”状态 + 队列”这个骨架抓住,再看 Semaphore、ReentrantReadWriteLock(把一个 state 拆成高低 16 位分别记录读写计数)、甚至线程池的 Worker,都只是同一套模板的变奏。