Skip to content

LockSupport 与 AQS:并发工具的骨架 ​

ReentrantLock、Semaphore、CountDownLatch——三种表面不同的工具,为什么源码都藏在同一个基类里?CyclicBarrier 为什么不继承它?

第 6 章的 synchronized把互斥锁封装在 JVM 内部。开发者只有一个开关:synchronized/不 synchronized。这一章讨论的是另一条路:把锁的实现搬到 Java 代码层面,让"如何挂起线程"、"如何组织等待队列"、"如何唤醒"这些机制变得可编程。LockSupport 提供底层的线程挂起与唤醒机制,AQS 在其上封装同步状态、等待队列和获取释放流程。理解这两层之后,再看 ReentrantLock、Semaphore 和 CountDownLatch 的实现,会更容易区分它们共享的机制与各自的同步语义。

版本基线:本页按 JDK 21 的 AbstractQueuedSynchronizer 实现讲解。JDK 8 时代常见资料使用 waitStatus、SIGNAL、unparkSuccessor 和 PROPAGATE;JDK 21 已改用 Node.status、WAITING、CANCELLED、COND 与 signalNext。概念模型仍然相通,但源码名称和传播路径不能混用。

1. synchronized 走不到的地方 ​

1.1 五个 synchronized 做不到的诉求 ​

synchronized 的语义是"要么拿到锁进入临界区,要么阻塞等"。真到线上,业务几乎每天都在提出更细的要求:

需求仅使用 synchronized 这一监视器机制时的限制
"我最多等 3 秒,拿不到就走"做不到
"我等锁的时候允许被 interrupt 打断"做不到
"同一把锁,读线程之间不要互斥"做不到
"等的时间越久越优先,别让新来的插队"做不到
"生产者等'非满'、消费者等'非空',两条队列别搅在一起"做不到(一个 Monitor 只有一条等待队列)

不是 synchronized 设计得差,而是它把选择权全部下放到了 JVM 内部——JVM 只做互斥这一种语义。任何超出互斥的诉求,都需要一套 Java 层可编程的锁基础设施。

1.2 把锁搬到 Java 层,需要什么 ​

如果不再依赖 monitorenter/monitorexit,一把互斥锁至少要自己回答三个问题:

  • 状态用什么表达:几把锁、有没有人持锁、重入了几次
  • 争锁失败的线程放到哪里:等待队列的数据结构、入队方式
  • 挂起和唤醒怎么实现:谁把线程挂起、谁把它叫醒

第一个问题的答案是:一个 volatile int + CAS。第二个问题的答案是:一条 FIFO 双向链表。第三个问题的答案是:LockSupport.park / unpark。

这三样加在一起就是 AQS 的骨架。但要理解 AQS 的挂起/唤醒是怎么发生的,得先看第三样——LockSupport。

开篇问的 CyclicBarrier 为什么不继承 AQS:它通过 ReentrantLock 和 Condition 协调一组线程到达屏障后共同放行,并支持屏障重复使用,不需要自己实现同步状态和等待队列,因此直接组合而非继承。AQS 是构建同步器的工具,不是所有并发工具都必须继承的基类。

2. LockSupport:许可证式挂起 ​

2.1 park / unpark 的语义 ​

java.util.concurrent.locks.LockSupport 只有两个核心静态方法:

java
LockSupport.park();                 // 挂起当前线程
LockSupport.park(Object blocker);   // 同上,附带阻塞原因,dump 时能看到

LockSupport.unpark(Thread t);       // 唤醒指定线程

它对每条线程维护一张许可证(permit):

  • unpark(t) 把 t 的许可证置为"可用"
  • park() 检查当前线程的许可证:可用则消耗掉后立即返回;否则线程可能被挂起。之后 park() 可以因其他线程调用 unpark()、当前线程被中断,或发生虚假唤醒而返回,调用方必须重新检查等待条件,不能把 park() 返回等同于"等待条件已满足"

许可证是一个二值状态——不是计数器。许可证已可用时,再次 unpark 不会累积额外许可证。

2.2 三点关键差异 ​

LockSupport 与 Object.wait/notify 表面上都能挂起线程,但用途完全不同:

维度wait / notifypark / unpark
是否需要持锁必须持有对象的 Monitor,否则 IllegalMonitorStateException不需要
唤醒目标从 _WaitSet 里挑一个(notify 不确定,notifyAll 全部)精确指定某条线程
unpark / notify 早于挂起无效,notify 时如果没人在 wait,什么都不发生有效,许可证保留,后续 park 立即返回
中断行为抛 InterruptedExceptionpark 直接返回,需自行检查 Thread.interrupted()

"唤醒可以先于挂起" 是许可证语义带来的关键性质。用 wait/notify 实现一个"线程 A 通知 B"的原语,要小心 A 的 notify 早于 B 的 wait 的时序——一旦跑到前面,B 就会永远睡下去。park/unpark 不存在这个时序坑。

2.3 一个可以自己写的简易锁 ​

用 park/unpark 就能拼一把最简的互斥锁:

java
public class SimpleLock {
    private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters = new ConcurrentLinkedQueue<>();

    public void lock() {
        Thread current = Thread.currentThread();
        while (!locked.compareAndSet(false, true)) {
            waiters.offer(current);
            // 二次检查:可能就在这一瞬间锁被释放了
            if (locked.get()) {
                LockSupport.park(this);
            }
            waiters.remove(current);
        }
    }

    public void unlock() {
        locked.set(false);
        Thread next = waiters.peek();
        if (next != null) LockSupport.unpark(next);
    }
}

这段代码只是演示 CAS + 等待队列 + park/unpark 的组合思路,不是可直接用于生产的锁实现。它不保证公平性,不支持重入,也没有校验释放操作的线程所有权;中断处理和队列维护同样不完整。许可证机制可以解决部分检查与挂起之间的时序问题,但不能单独保证整个锁算法的正确性。它揭示的事实是:"CAS 抢状态 + 队列排队 + park/unpark 挂起唤醒"就是一把锁的最小工作集。AQS 做的事,本质上就是把这套模式提炼成一个可扩展的框架,并用节点状态、队列协议和取消清理逻辑协调竞争线程。

2.4 底层实现的一句话交代 ​

LockSupport.park 最终委托到 JVM 的线程挂起实现;具体系统调用会随 JDK 版本和操作系统变化,不能笼统地归结为某一个 POSIX 函数。挂起线程在 Java 层通常表现为 WAITING,在操作系统层面进入等待状态,不持续消耗 CPU。这也是 AQS 得以在未拿到锁的线程上不空转的技术底座。

3. AQS 的三件套 ​

3.1 一个 state + 一个 CLH 队列 + 一套 Node 状态 ​

AbstractQueuedSynchronizer(AQS)的内部结构:

txt
     ┌──────────────────────────────────────────────┐
     │        AbstractQueuedSynchronizer            │
     │                                              │
     │   volatile int state          ← 同步状态       │
     │                                              │
     │   Node head ─→ Node ─→ Node ─→ Node ← tail   │
     │        │        │       │       │            │
     │        │     thread=B thread=C thread=D     │
     │        │     status=1 status=1    status=0   │
     │      (虚节点)                                 │
     └──────────────────────────────────────────────┘
  • state:volatile int。语义由子类决定——ReentrantLock 里它是"重入次数",Semaphore 里它是"剩余许可数",CountDownLatch 里它是"未完成计数"。修改通过 compareAndSetState 保证原子性。
  • CLH 队列:一条 FIFO 双向链表,节点类型是 Node。抢锁失败的线程被封装成 Node 挂到队尾。队头是一个"虚节点"(sentinel),当前持锁线程本身不在队列中——head 是"下一个要被唤醒的候选人的前驱"。
  • Node 的 status:一个 volatile int,用位组合表示等待、取消和条件等待状态。

3.2 Node 的状态位(JDK 21) ​

状态位值含义
WAITING1等待线程可能正在 park,唤醒方需要清除该位再 unpark
CANCELLED0x80000000节点对应的获取操作已取消,例如因超时或可中断获取被中断而退出,该节点不再参与正常获取流程
COND2节点当前处于 Condition 等待
00未设置上述状态位;能否安全 park 仍需配合队列位置复核

JDK 21 通过原子位操作避免 park/unpark 竞态:等待线程先设置 WAITING 再复核,唤醒方清除 WAITING 后调用 unpark。旧资料中的"四状态 waitStatus"对应的是另一版实现,不能直接套到这里的常量表上。上表列出的是 JDK 21 AQS 内部实现使用的状态值,不是 AbstractQueuedSynchronizer 的公共 API 契约,理解时应关注这些状态如何参与等待、取消与唤醒协议。

3.3 模板方法模式:分离"如何获取"与"失败后怎么办" ​

AQS 用模板方法把工作切成两半:

AQS 已经写好的(不变部分):

  • CLH 队列的入队 / 出队 / 前驱状态维护
  • LockSupport.park / unpark 挂起唤醒
  • 中断响应
  • 超时控制
  • 公平/非公平策略骨架

子类需要实现的(变化部分):

方法语义何时实现
tryAcquire(int)独占模式:尝试获取,成功 true 失败 falseReentrantLock 等独占锁
tryRelease(int)独占模式:尝试释放,true 表示同步器已完全释放独占锁
tryAcquireShared(int)共享模式:负数失败,零成功但后续可能失败,正数成功且后续可能成功Semaphore / CountDownLatch
tryReleaseShared(int)共享模式:释放并返回是否需要传播共享同步器
isHeldExclusively()当前线程是否独占持有支持 Condition 时需实现

子类不需要碰队列、park、中断处理——那些 AQS 已经解决过一次,之后所有子类共用。AQS 只提供同步框架,子类仍必须正确实现状态转换、并发安全和释放语义;只要子类能用 state 表达同步条件,并正确实现获取、释放与必要的所有权检查,就可以复用 AQS 的队列和等待机制。

4. 独占模式与共享模式 ​

AQS 从入口就分成两条路径:acquire / release 走独占,acquireShared / releaseShared 走共享。骨架相同,唤醒策略不同。

4.1 独占模式:先尝试,再入队,再挂起 ​

ReentrantLock 是独占模式的典型。acquire 的骨架:

java
public final void acquire(int arg) {
    if (!tryAcquire(arg))
        acquire(null, arg, false, false, false, 0L);
}

核心过程仍是“尝试、入队、复核并挂起”,但 JDK 21 将这些步骤集中到内部 acquire(...) 方法,不再沿用旧版 addWaiter + acquireQueued 的公开结构:

第一步 tryAcquire(子类实现):非公平模式直接 CAS 抢,公平模式先看队列有没有前驱。

java
// NonfairSync 非公平模式
final boolean nonfairTryAcquire(int acquires) {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    } else if (current == getExclusiveOwnerThread()) {   // 重入
        setState(c + acquires);
        return true;
    }
    return false;
}
java
// FairSync 公平模式:多一行前驱检查
if (c == 0) {
    if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

第二步(入队):把当前线程包成 Node 挂到队尾。快速路径是“tail 非空 + 一次 CAS”,慢路径用自旋 CAS 处理“队列尚未初始化”和并发入队的边界。

第三步(自旋 + 挂起):

txt
重复:
  若自己是队首候选 → 再次 tryAcquire,成功则更新 head
  若前驱已取消     → 清理队列并重新检查
  若 status 尚未标记 → 设置 WAITING 并重新复核
  否则             → park,被 unpark 后检查中断/取消并重试

这段是根据 JDK 21 源码压缩后的流程说明,不是逐行方法名对照。关键契约是:线程在确认自己仍需等待后设置 WAITING,唤醒方必须先清除此位,避免唤醒与 park 发生竞态。

最终仍通过 LockSupport.park 挂起,直到唤醒方调用 unpark。

把这三步串起来,完整的 acquire 流程如下:

获取过程可能多次自旋:如果前驱状态为 CANCELLED,线程会调用清理逻辑跳过无效节点;确认进入等待后设置 WAITING,再 park 并复核。这个过程保证取消节点不会长期阻塞唤醒链。

4.2 独占模式的 release:只干两件事 ​

java
public final boolean release(int arg) {
    if (tryRelease(arg)) {                 // state 归零?
        signalNext(head);                  // 清 WAITING 并唤醒后继
        return true;
    }
    return false;
}

tryRelease 由子类决定“归零”的条件。ReentrantLock 里必须减到 0 才算真释放——重入了三次要 unlock 三次。归零后,signalNext 检查 head.next,原子清除后继的 WAITING 并调用 LockSupport.unpark。被唤醒的线程回到获取流程再次尝试。

JDK 21 的 signalNext 主要沿可见的 next 指针定位后继,并用 status 协调 park/unpark;取消节点的复杂清理由 cleanQueue 等路径处理。不要把旧版 unparkSuccessor “从 tail 回溯”的实现细节当作当前 JDK 的固定流程。

4.3 共享模式:唤醒之后还要接力 ​

Semaphore.acquire(1) / CountDownLatch.await 走共享路径:

java
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

tryAcquireShared 返回值语义与独占版不同:负数=失败,零=成功但后续共享获取可能失败,正数=成功且后续共享获取可能成功。这个返回值表达的是获取结果以及是否可能继续传播共享获取,不是普遍的"剩余资源数"。CountDownLatch 在计数归零后让等待线程通过,其返回值并不是把"剩余许可数"返回给调用方。

举一个 CountDownLatch 的场景:

txt
CountDownLatch(3):三条线程 A / B / C 都在 await
队列: head → NodeA → NodeB → NodeC ← tail

外部调用 countDown 三次,state = 0。
最后一次 countDown 唤醒 NodeA。

如果队首已经完成但仍存在其他共享等待者,就需要继续唤醒下一个共享节点。JDK 21 通过 SharedNode 类型和 signalNextIfShared 实现这一步。

共享模式与独占模式的关键区别,不是"唤醒所有等待者",而是一次成功获取或释放后,可能还有其他等待线程也具备获取条件。实现示意(概念流程,不是逐行源码对照):

txt
共享获取失败
  → 节点入队
  → 等待并重新尝试获取

共享获取成功
  → 将成功节点设为新的队头
  → 根据共享获取结果和队列状态,判断是否需要继续唤醒共享后继

signalNextIfShared 只唤醒 SharedNode 类型的后继:清除 WAITING,再调用 unpark。释放方的 releaseShared 同样调用 signalNext(head);配合重新获取后的 signalNextIfShared,让共享许可继续向后传递。是否继续传播取决于共享获取结果和队列状态,不是无条件唤醒所有后继节点。CountDownLatch 的计数归零后等待线程可以依次被唤醒并通过检查,Semaphore 则取决于剩余许可是否足够。JDK 21 的 AQS 实现不再使用旧版的 PROPAGATE 状态协议。

4.4 独占 vs 共享的核心差异 ​

维度独占模式共享模式
同一时刻持有者1多个
state 语义是否被占 / 重入计数剩余许可 / 未完成计数
获取失败判据tryAcquire 返回 falsetryAcquireShared 返回负数
释放后唤醒只唤醒队首后继唤醒后继并沿链传播
典型工具ReentrantLock / ReadWriteLock 写锁部分Semaphore / CountDownLatch / 读锁部分

5. 本页小结 ​

问题根源解决方案
需要超时、中断或公平获取synchronized 把选择权留在 JVM 内部LockSupport 将挂起与唤醒暴露给 Java 层
唤醒可能早于挂起造成丢失notify 没有许可证语义LockSupport.unpark 保留许可证
每种同步器都要重复实现等待队列队列与业务语义耦合AQS 抽出 state + CLH + park/unpark 骨架
独占与共享的唤醒规则不同单持有者与多持有者的传播方式不同共享模式通过 SharedNode + signalNextIfShared 继续传播

下一页从 Condition 的条件队列开始,再集中比较基于 AQS 的常用同步工具。

下一页: Condition 与 AQS 同步工具

Released under the MIT License.