Day10 AQS框架:ReentrantLock与CountDownLatch的共同秘密
专栏《Java后端工程师进阶之路》工作日每日一更。欢迎关注老梁。一、AQS 是什么并发包的底层操作系统如果把 JUC 并发包看作一座大厦AQS 就是地基和承重墙。它提供了一个模板框架维护一个volatile int state表示同步状态0无人持有1已被占用N重入次数或剩余许可数维护一个FIFO 双向队列CLH 锁队列的变体来管理拿不到锁、正在排队的线程定义了两种资源访问模式独占模式Exclusive和共享模式Shared子类只需要覆写几个方法就能快速实现各种同步器同步器模式覆写方法ReentrantLock独占tryAcquire/tryReleaseCountDownLatch共享tryAcquireShared/tryReleaseSharedSemaphore共享tryAcquireShared/tryReleaseSharedReentrantReadWriteLock独占共享读/写分别覆写AQS 核心架构可以用一张图概括图示AQS 包含 state 状态字段 CLH 变体队列队列节点 Node 包含 waitStatus、thread、prev/next 指针下方分支出独占模式与共享模式两类实现二、CLH 队列线程排队的秘密名册AQS 的队列不是简单的 ArrayBlockingQueue而是CLHCraig-Landin-Hagersten锁队列的变体特点双向链表每个 Node 有prev和next指针方便快速出队和逆向遍历取消节点时从尾部向前找虚拟头节点初始化时head指向一个空 Node不关联线程真正的等待线程从第二个节点开始排队状态驱动每个节点的waitStatus决定后续行为SIGNAL-1表示需要唤醒后继CANCELLED1表示已取消为什么用双向链表而不是单向因为取消节点时需要从尾部向前遍历找到合法前驱单向链表在尾部插入时无法快速修正前驱的 next 指针。三、实战代码 1ReentrantLock 公平 vs 非公平来看一个最直观的对比实验。下面的代码分别用公平锁和非公平锁启动 5 个线程竞争观察线程获取锁的顺序import java.util.concurrent.locks.ReentrantLock; public class FairnessDemo { // 切换 true/false 观察输出差异 private static final ReentrantLock lock new ReentrantLock(true); // 公平锁 // private static final ReentrantLock lock new ReentrantLock(false); // 非公平锁 public static void main(String[] args) { for (int i 0; i 5; i) { final int id i; new Thread(() - { lock.lock(); try { System.out.println(Thread- id 获取锁); // 故意不 sleep快速释放看竞争顺序 } finally { lock.unlock(); } }, Thread- i).start(); } } }运行结果对比公平锁true输出顺序一定是 Thread-0, Thread-1, Thread-2, Thread-3, Thread-4严格按照线程启动顺序排队获取非公平锁false顺序大概率被打乱后启动的线程可能插队直接拿到锁为什么非公平锁更快因为非公平锁在lock()时会先尝试一次 CAS 抢锁只有失败后才排队。如果刚好上一个线程释放锁新线程直接 CAS 成功省去了线程切换和队列操作的 overhead。代价是可能造成饥饿。源码级的差异只有一行公平锁的tryAcquire里多了!hasQueuedPredecessors()判断——只要有前驱节点在排队哪怕 state 是 0也老老实实去排队。四、源码走读 acquire() 的完整链路打开java.util.concurrent.locks.AbstractQueuedSynchronizer跟老梁一起走一遍acquire(int arg)方法public final void acquire(int arg) { if (!tryAcquire(arg) // ① 子类实现尝试获取锁 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // ② 失败则入队自旋/阻塞 selfInterrupt(); // ③ 补偿中断标志 }步骤拆解tryAcquire(arg)由子类如ReentrantLock.Sync实现。非公平锁直接 CAS公平锁先检查hasQueuedPredecessors()addWaiter(Node.EXCLUSIVE)将当前线程封装成 Node插入队列尾部。这里用尾插法 CAS 竞争先尝试一次快速入队失败则进入循环 CAScompareAndSetTailacquireQueued(node, arg)这是核心中的核心。节点入队后会检查自己的前驱是不是头节点。如果是再尝试一次tryAcquire如果不是或者获取失败则调用shouldParkAfterFailedAcquire判断是否需要阻塞最终调用LockSupport.park(this)挂起线程selfInterrupt()如果在等待期间收到了中断信号由于park()不会抛异常AQS 需要手动补一个Thread.currentThread().interrupt()来保留中断状态供上层业务感知释放锁的链路releasepublic final boolean release(int arg) { if (tryRelease(arg)) { // 子类实现state 减到 0 Node h head; if (h ! null h.waitStatus ! 0) unparkSuccessor(h); // 唤醒头节点的后继 return true; } return false; }注意唤醒时不是直接唤醒头节点因为头节点是虚拟节点threadnullAQS 唤醒的是head.next。图示非公平锁获取流程——直接 CAS → 成功执行业务 / 失败入队 → 自旋/阻塞 → 被唤醒再次尝试 → 成功出队公平锁获取流程——先检查前驱 → 有前驱则直接排队 → 无前驱才 CAS → 失败同样入队 → 严格 FIFO五、手写一个极简 AQS理解核心看源码容易晕不如自己写一个玩具版。下面这个MiniAQS去掉了所有边界处理只保留核心骨架帮你建立直觉import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.LockSupport; /** * 极简 AQS 教学版仅支持独占锁理解 state CLH 队列的核心机制 * JDK 版本无关纯逻辑演示 */ public class MiniAQS { private volatile int state 0; private volatile Node head; private volatile Node tail; private Thread exclusiveOwnerThread; // 当前持有锁的线程 static class Node { volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node(Thread thread) { this.thread thread; } } // 获取锁对应 acquire public void lock() { Thread current Thread.currentThread(); // 快速路径CAS 尝试获取 if (compareAndSetState(0, 1)) { exclusiveOwnerThread current; return; } // 慢速路径入队 自旋/阻塞 Node node addWaiter(); acquireQueued(node); } private Node addWaiter() { Node node new Node(Thread.currentThread()); // 简单的尾插法省略 CAS 竞争循环教学用 if (tail null) { head new Node(null); // 虚拟头节点 tail head; } node.prev tail; tail.next node; tail node; return node; } private void acquireQueued(Node node) { while (true) { Node p node.prev; if (p head compareAndSetState(0, 1)) { // 前驱是头节点且 CAS 成功则成为新的头节点 head node; head.thread null; // 头节点线程置空成为新虚拟头 head.prev null; exclusiveOwnerThread Thread.currentThread(); return; } // 否则阻塞等待前驱释放时唤醒 LockSupport.park(this); } } // 释放锁对应 release public void unlock() { if (Thread.currentThread() ! exclusiveOwnerThread) throw new IllegalMonitorStateException(); exclusiveOwnerThread null; state 0; // 唤醒后继节点 if (head ! null head.next ! null) { LockSupport.unpark(head.next.thread); } } private boolean compareAndSetState(int expect, int update) { // 教学简化实际用 Unsafe.compareAndSwapInt if (state expect) { state update; return true; } return false; } // 简单测试 public static void main(String[] args) throws InterruptedException { MiniAQS lock new MiniAQS(); for (int i 0; i 3; i) { final int id i; new Thread(() - { lock.lock(); try { System.out.println(Thread- id 执行业务); Thread.sleep(100); // 模拟业务 } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { lock.unlock(); } }).start(); } } }这个极简版虽然省略了中断处理、取消节点清理、CAS 竞争循环等细节但核心骨架完全一致state控制资源Node排队线程park/unpark实现阻塞唤醒。六、共享模式CountDownLatch 的奥秘AQS 的另一面是共享模式。CountDownLatch的await()和countDown()本质上就是共享资源的获取和释放state初始化为计数器的值如 3countDown()→tryReleaseShared→state--当 state 减到 0 时唤醒所有等待线程await()→tryAcquireShared→ 检查 state 是否为 0是则返回成功否则入队等待注意共享模式释放时会级联唤醒doReleaseShared因为多个线程可能同时在等这把共享钥匙。七、代码 3CountDownLatch 多线程聚合下面这段代码模拟一个常见的场景主线程等待三个子线程各加载完配置、缓存、数据库连接后再启动服务import java.util.concurrent.CountDownLatch; public class StartupCoordinator { private static final CountDownLatch latch new CountDownLatch(3); public static void main(String[] args) throws InterruptedException { new Thread(new Loader(配置中心, 500)).start(); new Thread(new Loader(本地缓存, 800)).start(); new Thread(new Loader(数据库连接池, 1200)).start(); System.out.println([主线程] 等待所有组件就绪...); latch.await(); // 阻塞直到 countDown 3 次 System.out.println([主线程] 所有组件就绪服务启动); } record Loader(String name, int costMs) implements Runnable { Override public void run() { try { System.out.println([ name ] 开始加载...); Thread.sleep(costMs); System.out.println([ name ] 加载完成); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { latch.countDown(); // 关键不管成功失败都要 countDown否则主线程永远等 } } } }输出[主线程] 等待所有组件就绪... [配置中心] 开始加载... [本地缓存] 开始加载... [数据库连接池] 开始加载... [配置中心] 加载完成 [本地缓存] 加载完成 [数据库连接池] 加载完成 [主线程] 所有组件就绪服务启动关键细节countDown()一定要放在finally块里否则子线程异常退出会导致主线程永久阻塞。生产环境中更推荐用CompletableFuture.allOf()或Phaser替代但理解 AQS 的共享模式是掌握这些高级工具的前提。建议3 条可落地的经验默认用非公平锁除非你有明确的公平性需求new ReentrantLock()默认非公平吞吐量更高。只有在严格顺序敏感的场景如排队扣库存才考虑公平锁。别为了感觉公平牺牲 10%~20% 的性能。lock() 必须在 finally 里 unlock()但 tryLock() 必须在 if 里 unlock()lock()和unlock()的配对大家都懂。但tryLock()如果没获取到锁千万别调用unlock()否则会抛IllegalMonitorStateException或把别人的锁给释放了。排查死锁时先 jstack 看 waiting on condition 和 parking to wait forAQS 阻塞的线程在 jstack 里显示为parking to wait for 某地址对应LockSupport.park。如果看到大量线程 park 在同一个 AQS 实例上说明锁竞争激烈或发生了死锁。下一篇Day 11CompletableFuture 异步编程告别回调地狱。我会用 20 个 API 的速查表 一个真实的订单编排案例带你彻底搞定 Java 异步编程的瑞士军刀。