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):显式锁的地基

两个核心组件

  1. volatile int state——同步状态(含义由子类定义):
    • ReentrantLock:state = 重入次数,0 是无锁;
    • CountDownLatch:state = 还差的计数;
    • Semaphore:state = 剩余许可数;
  2. 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;
    }
}

三个容易被问的点:

  1. 为什么死循环里要 park? 头两个自旋(前驱是 head)是"再给一次机会"的乐观抢锁,之后 shouldPark 把前驱标记成 SIGNAL 再 park——避免无限空转烧 CPU;
  2. SIGNAL 是什么? 前驱节点的 waitStatus=-1 表示"我释放时会 unpark 你"——释放锁的人只唤醒队头,队头拿到锁后负责唤醒下一个,像接力棒;
  3. 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-并发容器与工具类