--- title: "04-Lock与AQS" created: 2026-09-01 tags: - Java --- # Lock与AQS > [!note] 本篇定位 > 从"会用 ReentrantLock"到"懂 AQS 原理"两级:先用对比表建立 Lock 与 synchronized 的选型认知,再拆 Condition 等待通知,最后进 AQS 核心(**state + CLH 队列 + 独占/共享模式**)——AQS 是 CountDownLatch/Semaphore/ReentrantLock 共同的地基,面试区分度最高的一篇。 ## ReentrantLock 基本用法 ```java import java.util.concurrent.locks.ReentrantLock; public class LockDemo { private static int count = 0; private static final ReentrantLock lock = new ReentrantLock(); public static void main(String[] args) throws Exception { Runnable r = () -> { for (int i = 0; i < 10000; i++) { lock.lock(); // 加锁必须紧跟 try try { count++; } finally { lock.unlock(); // unlock 必须放 finally,否则异常时锁永远不释放 } } }; Thread t1 = new Thread(r), t2 = new Thread(r); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(count); // 稳定输出 20000 } } ``` ## ReentrantLock vs synchronized | 维度 | synchronized | ReentrantLock | | --- | --- | --- | | 层级 | JVM 关键字(monitorenter) | JDK 类(AQS) | | 释放 | 自动(异常也会释放) | **必须手动 unlock()**(忘了解锁是重 bug) | | 公平锁 | 仅非公平 | 可选公平/非公平(构造参数) | | 可中断 | ❌ 阻塞中不可中断 | `lockInterruptibly()` 可中断 | | 超时 | ❌ | `tryLock(3, SECONDS)` | | 条件队列 | 一个(wait/notify) | **多个 Condition**,精确唤醒 | | 性能 | JDK 6 优化后基本持平 | 基本持平 | 选型默认 synchronized(简单不易错),需要**公平、可中断、超时、多条件**时才上 ReentrantLock。 ## Condition:精准唤醒 ```java ReentrantLock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition(); // 生产者:队列满时 notFull.await();放入元素后 notEmpty.signal() // 消费者:队列空时 notEmpty.await();取走元素后 notFull.signal() ``` 对比 wait/notify:notify 只能随机唤醒一个,Condition 可以按条件分开唤醒——这是 ArrayBlockingQueue 内部的实现方式。 ## AQS(AbstractQueuedSynchronizer):显式锁的地基 **两个核心组件**: 1. **volatile int state**——同步状态(含义由子类定义): - ReentrantLock:state = 重入次数,0 是无锁; - CountDownLatch:state = 还差的计数; - Semaphore:state = 剩余许可数; 2. **CLH 变体的 FIFO 双向队列**——抢锁失败的线程封装成 Node 排队,队首依次唤醒。 **独占模式获取流程(lock())**: ```text CAS 把 state 0→1 成功?──是──▶ 拿到锁(owner 记录自己,支持重入:state++) │ 否 ▼ tryAcquire 失败 → addWaiter 入队尾部 → park 挂起 │ 前驱释放锁 ▼ 被 unpark 唤醒 → 再次 tryAcquire → 成功则出队 ``` **共享模式**(acquireShared/releaseShared):state > 0 时可多个线程同时通过——CountDownLatch 的 `countDown()` 就是 state--,减到 0 唤醒所有等待线程;Semaphore 同理。 **公平 vs 非公平的区别就一行**:非公平的 tryAcquire 上来直接 CAS 抢(插队,吞吐高);公平的先 `hasQueuedPredecessors()` 判断队里有没有人,有就老实排队。 ## AQS 源码深读:从 state 到队列的完整链路 > [!note] 本节定位 > 上一节是"原理图",这节是"源码地图"——把 lock() 一条调用链走到底,看懂 AQS 骨架后,CountDownLatch/Semaphore 都只是"换个 state 含义 + 换个 try 方法"而已。重点记**三件套:state、队列、模板方法钩子**。 **一、骨架:AQS 的字段与模板方法模式** ```java public abstract class AbstractQueuedSynchronizer { private transient volatile Node head; // 队头(持锁线程的哨兵) private transient volatile Node tail; // 队尾(最新排队的) private volatile int state; // 同步状态,volatile,CAS 改 static final class Node { volatile Node prev, next; // 双链表 volatile Thread thread; // 排队线程 volatile int waitStatus; // 0 / SIGNAL(-1) / CANCELLED(1) 等 Node nextWaiter; // 共享/条件队列复用 } } ``` AQS 是**模板方法模式**:骨架(排队、唤醒、CAS state)写在 AQS,子类只需实现"怎么算拿锁成功"的钩子: ```text 独占模式要重写: tryAcquire(state) / tryRelease(state) 共享模式要重写: tryAcquireShared(state) / tryReleaseShared(state) 是否独占: isHeldExclusively()(公平/条件队列用它) ``` 所以 ReentrantLock 的内部类 `Sync extends AQS` 只写 tryAcquire/tryRelease("state>0 代表有人持锁,重入就 state++"),排队逻辑全是 AQS 给的——**"看懂 AQS 等于看懂半个 JUC"就是这个意思**。 **二、lock() 调用链:ReentrantLock 非公平锁** ```java // ReentrantLock.NonfairSync final void lock() { if (compareAndSetState(0, 1)) // ① 先插队抢一次 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // ② 抢不到走正式流程 } // AQS.acquire —— 模板方法,独占模式主流程 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // ③ 入队:CAS 把当前线程包装成 Node 挂到队尾 private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { // 快路径:尾节点不为空直接 CAS 挂尾 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); // 慢路径:队列未初始化走自旋 enq 初始化 return node; } // ④ 排队自旋:挂起或被唤醒后反复抢 final boolean acquireQueued(final Node node, int arg) { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { // 前驱是头并且抢到 → 出队当新头 setHead(node); p.next = null; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && // 前驱 waitStatus 置 SIGNAL parkAndCheckInterrupt()) // LockSupport.park 挂起 interrupted = true; } } ``` 三个容易被问的点: 1. **为什么死循环里要 park?** 头两个自旋(前驱是 head)是"再给一次机会"的乐观抢锁,之后 `shouldPark` 把前驱标记成 SIGNAL 再 park——避免无限空转烧 CPU; 2. **SIGNAL 是什么?** 前驱节点的 waitStatus=-1 表示"我释放时会 unpark 你"——释放锁的人只唤醒队头,队头拿到锁后负责唤醒下一个,像接力棒; 3. **head 节点是谁?** 通常是"已拿到锁或已释放锁"的空壳节点,队列的哨兵作用,真正的等待者从 head.next 开始。 **三、释放与唤醒:release() 链路** ```java public final boolean release(int arg) { if (tryRelease(arg)) { // ① 子类:ReentrantLock 就是 state--,减到 0 清 owner Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // ② 唤醒队列里真正在等的第一个线程 return true; } return false; } ``` 唤醒后那个线程回到 `acquireQueued` 的自旋里继续抢——所以锁的传递不是"直接移交",而是"唤醒 + 再竞争",**非公平就在这**:刚被唤醒的线程可能抢不过场外刚进来 CAS 的新线程。 **四、共享模式与可重入的关键差异** - **共享释放的传播**:`tryAcquireShared` 返回剩余资源数,抢到且剩余>0 时会继续唤醒下一个共享节点(`setHeadAndPropagate`)——这就是 CountDownLatch 减到 0 后"一波全醒"的源码来源; - **可重入的计数**:tryAcquire 里 `getState()+1`,Release 里 `getState()-1`,减到 0 才真正释放 owner——所以 ReentrantLock 加几次锁就要解几次锁,少一次就死锁([[02-synchronized与volatile|02-synchronized与volatile]] 的死锁节同理)。 ## 基于 AQS 的三个常用协作工具 ```java // 1. CountDownLatch:倒数计数,"等 N 件事全部完成" CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { new Thread(() -> { /* 干活 */ latch.countDown(); }).start(); } latch.await(); // 阻塞到 state 减到 0 System.out.println("全部完成"); // 2. CyclicBarrier:凑够 N 个人一起走(可复用) CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("人齐了,开跑")); // 各线程 barrier.await();计数归零后自动重置,可进下一轮 // 3. Semaphore:限流,"N 个许可轮流用" Semaphore sp = new Semaphore(2); // 同时最多 2 个线程 sp.acquire(); try { /* 最多 2 个并发进入 */ } finally { sp.release(); } ``` **三者对比**:CountDownLatch **一次性**、面向"事件完成数";CyclicBarrier **可重复**、面向"线程集合点";Semaphore **可复用**、面向"资源数量"。 --- ⬅️ [[03-线程池|03-线程池]] 🏠 [[00-Java|00-Java]] ➡️ [[05-并发容器与工具类|05-并发容器与工具类]]