别再死磕 ReentrantLock 了,AQS 才是并发包的真正操盘手

发布时间:2026/6/3 1:53:07

别再死磕 ReentrantLock 了,AQS 才是并发包的真正操盘手 别再死磕 ReentrantLock 了AQS 才是并发包的真正操盘手前言你是不是也这样面试前背八股文把 ReentrantLock 的公平锁、非公平锁背得滚瓜烂熟。结果面试官问一句“底层到底怎么排队”你瞬间卡壳只能支支吾吾说“有个队列”。其实Java 并发包JUC里真正的“大 BOSS”不是 Lock 接口而是 AQS。ReentrantLock、CountDownLatch、Semaphore这些你天天用的工具本质上都是 AQS 的“皮肤”。不懂 AQS就像学开车只懂踩油门不懂发动机原理。一旦生产环境出现线程死锁、性能抖动你只能干瞪眼。今天咱们不背源码我用大白话把 AQS 这层窗户纸给你捅破。一、底层原理1.1 核心机制AQS 的全称是AbstractQueuedSynchronizer抽象队列同步器。名字听着唬人其实它只做两件事管理状态管理排队。你可以把 AQS 想象成一家“网红奶茶店”的排队系统。state 变量就是“奶茶制作台的状态”。0 代表空闲1 代表有人正在做。CLH 队列就是“门口的排队小火车”。线程想喝奶茶获取锁先看制作台有没有人。没人直接拿走状态抢锁成功。有人别硬挤去门口排队加入同步队列。graph TD A[线程请求锁] -- B{state 是否为 0?} B -- 是 -- C[修改 state 为 1] C -- D[获取锁成功] B -- 否 -- E[封装成 Node 节点] E -- F[加入 CLH 等待队列尾部] F -- G[挂起当前线程] G -- H[被前驱节点唤醒] H -- B D -- I[执行业务逻辑] I -- J[释放锁 state 变 0] J -- K[唤醒后继节点]这张图就是 AQS 的核心灵魂。它利用volatile int state保证可见性。利用Node节点双向链表实现 FIFO 排队。利用LockSupport.park()让排不到队的线程“睡觉”不浪费 CPU。1.2 与同类方案的对比在 AQS 出来之前大家写锁都是各自为战。有的用synchronized有的用LockSupport手写。AQS 统一了标准咱们对比一下特性原生 synchronized早期手写 LockAQS 框架排队管理JVM 内部处理不可控需自己维护链表内置 CLH 队列开箱即用状态管理隐式无法获取需 volatile 手动维护提供 protected state 字段扩展性低无法自定义公平性高但重复造轮子极高模板方法模式适用场景简单代码块同步特殊需求构建锁、信号量、栅栏说白了AQS 就是并发包的“基础设施”。它把排队、挂起、唤醒这些脏活累活都干了。你只需要关注“什么时候能拿锁”剩下的交给它。二、快速上手咱们不整虚的直接看一个最简化的 AQS 使用示例。注意生产环境别直接继承 AQS通常是用现成的 Lock。但为了理解原理咱们手写一个“简易独占锁”。import java.util.concurrent.locks.AbstractQueuedSynchronizer; // 定义一个简易锁继承 AQS class SimpleLock { // 内部类继承 AQS负责核心同步逻辑 private final Sync sync new Sync(); // 锁的入口 public void lock() { sync.acquire(1); } // 锁的出口 public void unlock() { sync.release(1); } // 核心同步器重写 AQS 的模板方法 class Sync extends AbstractQueuedSynchronizer { // 尝试获取锁返回 true 表示成功false 表示失败 Override protected boolean tryAcquire(int arg) { // 原子比较并修改 stateCAS 操作 if (compareAndSetState(0, 1)) { // 设置独占线程方便后续重入判断 setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 尝试释放锁 Override protected boolean tryRelease(int arg) { // 只有当前持有锁的线程才能释放 if (Thread.currentThread() ! getExclusiveOwnerThread()) { throw new IllegalMonitorStateException(当前线程不持有锁); } // 把状态重置为 0 setState(0); return true; } // 判断当前线程是否持有锁用于重入支持 Override protected boolean isHeldExclusively() { return getExclusiveOwnerThread() Thread.currentThread(); } } }这段代码只有几十行但把 AQS 的骨架全展示了。tryAcquire和tryRelease是你唯一需要关心的业务逻辑。至于怎么排队、怎么 park、怎么 unparkAQS 的acquire和release方法已经帮你写死了。这就是模板方法模式的力量。三、核心 API / 深水区3.1 核心方法速查玩透 AQS你只需要记住这几个关键方法。它们是自定义同步器的“七伤拳”。方法名作用备注getState()获取同步状态读取 volatile intsetState(int)设置同步状态直接赋值非原子compareAndSetState(int, int)CAS 更新状态核心原子操作必须用这个acquire(int)独占式获取如果失败会进入队列等待release(int)独占式释放唤醒后继节点addWaiter(Node.EXCLUSIVE)添加等待节点将线程封装成 Node 入队LockSupport.park()挂起线程让排队的线程休眠3.2 生产级配置在实际开发中直接继承 AQS 的情况较少。更多是配置ReentrantLock的公平性。这里有个大坑公平锁性能通常比非公平锁差 10 倍以上。// 非公平锁默认推荐吞吐量更高 ReentrantLock lock new ReentrantLock(); // 公平锁按排队顺序获取防止线程饥饿但性能有损耗 ReentrantLock fairLock new ReentrantLock(true);异常处理细节AQS 框架本身会处理InterruptedException。如果你重写tryAcquire千万不要捕获异常后吞掉必须向上抛出或正确恢复中断状态。否则会导致线程“假死”永远醒不过来。3.3 高级定制AQS 支持两种模式独占Exclusive和共享Shared。ReentrantLock是独占CountDownLatch是共享。如果你想写一个“读写锁”就需要同时实现这两种模式。tryAcquireShared返回负数表示失败0 表示成功但不唤醒后继正数表示成功且唤醒。这个返回值的设计非常精妙控制了唤醒的粒度。四、实战演练咱们来点硬的。模拟一个“秒杀系统”的库存扣减场景。要求同一时间只能有一个线程修改库存且必须保证原子性。我们用 AQS 实现一个“库存锁”。import java.util.concurrent.locks.AbstractQueuedSynchronizer; public class StockLock { // 内部同步器 private final Sync sync new Sync(); public void lock() { sync.acquire(1); } public void unlock() { sync.release(1); } class Sync extends AbstractQueuedSynchronizer { // 尝试获取锁 Override protected boolean tryAcquire(int arg) { // 模拟库存检查这里简化为只要 state 为 0 即可 if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); return true; } return false; } // 尝试释放锁 Override protected boolean tryRelease(int arg) { if (Thread.currentThread() ! getExclusiveOwnerThread()) { throw new IllegalMonitorStateException(非法释放); } setState(0); return true; } } // 模拟秒杀业务 public void reduceStock(int stockId) { lock(); try { // 模拟数据库操作耗时 Thread.sleep(100); System.out.println(Thread.currentThread().getName() 扣减库存成功ID: stockId); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { // 务必在 finally 中释放锁防止死锁 unlock(); } } }结果分析如果有 10 个线程同时调用reduceStock。只有一个线程能打印“扣减成功”。其他 9 个线程会在acquire处排队。前一个线程执行完unlock后一个线程才会被唤醒并执行。这就保证了库存不会超卖。五、避坑指南与最佳实践AQS 虽然强大但用不好就是灾难。以下是我踩过的坑你直接拿去用。技巧重入锁的实现如果你想实现重入同一个线程多次获取锁tryAcquire里要判断if (getExclusiveOwnerThread() Thread.currentThread())如果是直接setState(getState() 1)即可不用 CAS 争抢。⚠️警告不要修改 state 的可见性AQS 内部的state是volatile的。你在tryAcquire里读取其他共享变量时也要确保它们是volatile或者被锁保护。否则会出现“锁拿到了但数据是旧的”这种诡异问题。✅推荐使用 Condition 配合AQS 提供了newCondition()方法。这相当于Object.wait/notify的升级版。在tryAcquire失败时如果需要等待特定条件比如库存大于 0用 Condition 挂起比死循环轮询高效得多。六、综合实战演示最后咱们整合一下。写一个“多消费者单生产者”的场景。使用 AQS 的共享模式Shared来控制信号量。import java.util.concurrent.locks.AbstractQueuedSynchronizer; public class ResourcePool { private final int maxResources; private final Sync sync; public ResourcePool(int maxResources) { this.maxResources maxResources; this.sync new Sync(maxResources); } // 获取资源 public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } // 释放资源 public void release() { sync.releaseShared(1); } class Sync extends AbstractQueuedSynchronizer { public Sync(int state) { setState(state); } // 共享模式尝试获取 Override protected int tryAcquireShared(int arg) { // 尝试减少可用资源数 for (;;) { int available getState(); int remaining available - arg; // 资源不足返回 -1 表示失败 if (remaining 0) { return -1; } // CAS 更新状态 if (compareAndSetState(available, remaining)) { return remaining; // 返回剩余资源数0 表示唤醒所有 } } } // 共享模式尝试释放 Override protected boolean tryReleaseShared(int arg) { // 增加可用资源 setState(getState() arg); return true; // true 表示需要唤醒后继 } } }这段代码就是一个简易的Semaphore。tryAcquireShared返回负数会让线程进入等待队列。返回正数会触发doReleaseShared唤醒后续节点。这就是 AQS 共享模式的精髓一个释放全员唤醒或按需唤醒。七、总结AQS 不是魔法它只是把“排队”和“状态”抽象成了标准流程。记住三点state是资源volatile保证可见。CLH 队列是排队的地方LockSupport是睡觉的工具。重写tryAcquire和tryRelease剩下的交给 AQS 框架。下次再看到ReentrantLock别只当它是锁。想想它背后那个默默排队、睡觉、被唤醒的 AQS 引擎。搞懂了它JUC 包里的其他工具你一眼就能看穿本质。

相关新闻