Lock与AQS
本篇定位
从"会用 ReentrantLock"到"懂 AQS 原理"两级:先用对比表建立 Lock 与 synchronized 的选型认知,再拆 Condition 等待通知,最后进 AQS 核心(state + CLH 队列 + 独占/共享模式)——AQS 是 CountDownLatch/Semaphore/ReentrantLock 共同的地基,面试区分度最高的一篇。
ReentrantLock 基本用法
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:精准唤醒
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):显式锁的地基
两个核心组件:
- volatile int state——同步状态(含义由子类定义):
- ReentrantLock:state = 重入次数,0 是无锁;
- CountDownLatch:state = 还差的计数;
- Semaphore:state = 剩余许可数;
- CLH 变体的 FIFO 双向队列——抢锁失败的线程封装成 Node 排队,队首依次唤醒。
独占模式获取流程(lock()):
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 到队列的完整链路
本节定位
上一节是"原理图",这节是"源码地图"——把 lock() 一条调用链走到底,看懂 AQS 骨架后,CountDownLatch/Semaphore 都只是"换个 state 含义 + 换个 try 方法"而已。重点记三件套:state、队列、模板方法钩子。
一、骨架:AQS 的字段与模板方法模式
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,子类只需实现"怎么算拿锁成功"的钩子:
独占模式要重写: tryAcquire(state) / tryRelease(state)
共享模式要重写: tryAcquireShared(state) / tryReleaseShared(state)
是否独占: isHeldExclusively()(公平/条件队列用它)
所以 ReentrantLock 的内部类 Sync extends AQS 只写 tryAcquire/tryRelease("state>0 代表有人持锁,重入就 state++"),排队逻辑全是 AQS 给的——"看懂 AQS 等于看懂半个 JUC"就是这个意思。
二、lock() 调用链:ReentrantLock 非公平锁
// 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;
}
}
三个容易被问的点:
- 为什么死循环里要 park? 头两个自旋(前驱是 head)是"再给一次机会"的乐观抢锁,之后
shouldPark把前驱标记成 SIGNAL 再 park——避免无限空转烧 CPU; - SIGNAL 是什么? 前驱节点的 waitStatus=-1 表示"我释放时会 unpark 你"——释放锁的人只唤醒队头,队头拿到锁后负责唤醒下一个,像接力棒;
- head 节点是谁? 通常是"已拿到锁或已释放锁"的空壳节点,队列的哨兵作用,真正的等待者从 head.next 开始。
三、释放与唤醒:release() 链路
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 的死锁节同理)。
基于 AQS 的三个常用协作工具
// 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-线程池 🏠 00-Java ➡️ 05-并发容器与工具类
💬 评论