← blog 技术原理与实践 · 2026-10-10

从 synchronized 到 CountDownLatch:Java 同步机制底层实现串讲

自底向上拆解 Java 并发同步的完整链路:synchronized 的 Mark Word 与锁升级、AQS 的 state 与 CLH 队列、ReentrantLock 的独占模式、CountDownLatch 的共享模式与级联唤醒,最后给出三者对比与选型建议。

16 min read

面试里最经典的一串追问大概是这样的: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 是线程协作工具——它不保护临界区,只协调”什么时候一起走”。

维度synchronizedReentrantLockCountDownLatch
实现层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 的角色可以完全分离——计数的线程可以不等,等待的线程也可以不计数。这是锁语义覆盖不了的能力。

写在最后

回头看这条链路,其实只有两句话:

  1. synchronized 是 JVM 层的”状态 + 等待队列”(Mark Word + ObjectMonitor),AQS 是类库层的”状态 + 等待队列”(state + CLH 队列)——同一种思想,两个层次;
  2. AQS 之上,ReentrantLock 用独占模式(boolean 语义、唤醒单个),CountDownLatch 用共享模式(int 三态、级联传播)——同一个框架,子类只定义语义,流程全部复用。

把”状态 + 队列”这个骨架抓住,再看 Semaphore、ReentrantReadWriteLock(把一个 state 拆成高低 16 位分别记录读写计数)、甚至线程池的 Worker,都只是同一套模板的变奏。

Sources

No external sources for this entry.

Related

On this page 一、synchronized:JVM 层的原生实现1.1 字节码层:monitorenter / monitorexit1.2 对象头:Mark Word1.3 运行时层:锁升级1.4 其他优化与语义保证二、先澄清一个误区:synchronized 不是 AQS 实现的三、AQS:整个 J.U.C 的骨架3.1 state:同步状态3.2 CLH 变体队列3.3 模板方法模式四、ReentrantLock:AQS 的独占模式4.1 加锁流程(非公平为例)4.2 tryAcquire:可重入与公平性的全部秘密4.3 入队与挂起4.4 解锁五、CountDownLatch:AQS 的共享模式5.1 结构:state 就是倒计时5.2 tryAcquireShared 的三态语义5.3 countDown:并发的 CAS 减法5.4 级联唤醒:setHeadAndPropagate六、三者对比与选型性能与选型写在最后