AQS核心机制详解:从队列到模板方法的并发基石

发布时间:2026/10/17 13:10:26

AQS核心机制详解:从队列到模板方法的并发基石 1. AQS到底是什么为什么所有并发大神都绕不开它如果你在Java并发编程里摸爬滚打过一阵子一定见过AbstractQueuedSynchronizer这个名字圈内人习惯叫它AQS。它就像并发工具背后的地基你平时用的ReentrantLock、CountDownLatch、Semaphore甚至ThreadPoolExecutor里的Worker线程底层都是踩着AQS的肩膀在干活。一句话说清楚AQS是一个用来构建锁和同步器的框架它把“线程排队等待”、“阻塞与唤醒”、“共享或独占状态管理”这些最繁琐、最容易出错的底层逻辑全部封装好你只需要重写几个简单方法就能造出一把自定义的锁或者同步器。我第一次接触AQS的时候读源码读得头皮发麻。acquire、release、tryAcquire、tryRelease、CLH队列、Node、state……一堆概念扑面而来。但后来我换了一个思路不再纠结每一行代码而是先把AQS当成一个“线程排队管理器”来看瞬间就通透了很多。这篇文章我就打算用这种“先建立心智模型再深入代码细节”的方式来拆解AQS。不管你是刚接触并发不久的新手还是已经写过几个并发工具但总觉得隔着一层窗户纸的同学这篇内容应该都能帮到你。我会从设计思路讲起再落到核心源码然后结合真实场景分析它是怎么支撑起JDK里那些经典同步器的最后把我自己踩过的坑也一并分享出来。2. 为什么需要AQS或者说没有AQS之前我们在干什么2.1 手写一把锁的痛苦与重复假设你现在需要一把自定义锁你最朴素的想法是什么用一个volatile变量存状态0表示没人持有1表示有人持有然后用CAS去抢。抢不到就让线程自旋或者睡一会儿。听起来不难但如果你认真往深了想问题马上就来了状态变量发生竞争时CAS失败后线程怎么办是自旋还是阻塞自旋多久合适如果多个线程都在等锁锁释放时应该唤醒谁先进来的先获得吗如果需要公平性怎么办线程阻塞之后怎么恢复谁来负责park和unpark如果在唤醒过程中又有新线程来抢锁会不会出现线程饿死如果锁是可重入的state要累加释放时要递减这个逻辑写在哪里这些问题是每一把锁都必须面对的但如果你每写一种同步器就从头实现一遍大部分精力都消耗在了“线程排队与唤醒”这种通用逻辑上而不是你自己的业务规则上。有一说一那个年代造锁确实就是这么痛苦代码量巨大不说隐蔽的并发Bug一抓一大把。2.2 AQS的破局思路AQS的破局思路其实很朴素把同步器的通用骨架做好把个性化规则留给你。它内部维护了一个volatile int state代表同步状态。比如对于互斥锁state 0表示未锁定state 1表示已锁定state 1表示重入次数。对于信号量state直接就是剩余许可证数量。至于这个“同步状态”怎么解释、怎么改变AQS不替你决断它把tryAcquire、tryRelease这些方法设计成模板方法由子类去实现。而所有线程的排队、阻塞、唤醒、取消排队这一整套流程AQS用一个FIFO队列全部接管了。这个队列在源码里叫CLH锁队列的变体后面我会详细讲。打个比方AQS就像一家餐厅的排队叫号系统。它不管你餐厅卖的是火锅还是日料独占锁还是共享锁只管顾客怎么取号、怎么排队、听到叫号后怎么入座。每家餐厅自己的规则几个人一桌、有没有会员优先留给餐厅自己定义。3. 深入AQS核心机制状态、队列与模板方法3.1 同步状态state一切竞争的起点state是AQS里最核心的字段声明为volatile int。为什么必须是volatile因为多个线程可能同时读取和修改这个值一个线程改了其他线程必须立即可见否则就会出现“锁明明被释放了等待线程却看不到”这种致命问题。volatile保证了可见性但不保证原子性。所以AQS里所有对state的修改在竞争条件下都必须走compareAndSetState这种CAS操作或者走持有锁之后才允许执行的普通赋值操作。state的语义完全由子类定义这一点特别重要。我举个例子ReentrantLock里state表示持有锁的次数。初始0加锁成功变1重入一次加1解锁一次减1减到0才真正释放。CountDownLatch里state表示剩余需要等待的计数。每次countDown就减1减到0时唤醒所有等待线程。Semaphore里state表示剩余许可证数量。acquire时减1release时加1。所以千万不要把state简单理解成“锁的标志位”它更像是一个“语义由使用者决定”的通用计数器。AQS只负责保证对这个计数器的并发修改是安全的至于计数的含义是你自己的事。3.2 CLH变体队列线程等待的载体当CAS抢锁失败后线程并不会立刻阻塞而是先被包装成一个Node节点挂到一个FIFO队列尾部然后才考虑要不要阻塞自己。这个队列就是AQS的等待队列。这个队列的设计有很多细节值得品味队列头节点是哨兵节点或者叫哑节点它不代表任何等待线程只是方便队列操作的统一处理。节点之间的连接方式不是简单的prev/next双向链还有一个nextWaiter指针用来标记是独占模式还是共享模式。每个节点内部有一个waitStatus字段表示当前节点的等待状态取值包括CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)和0。为什么需要这个队列因为自旋等待只在短临界区里效率高如果锁被持有时间较长自旋就是白白消耗CPU。AQS的策略是先 CAS 尝试一次失败以后进入队列然后根据前驱节点的状态决定是否阻塞自己。这个“先用自旋抢救一下抢救不了再去睡觉”的思路在很多高性能框架里都能看到本质是在空转代价和上下文切换代价之间做平衡。3.3 模板方法设计把变化留给子类AQS采用了非常典型的模板方法模式。它定义了一套完整的算法骨架比如acquire的流程是先调tryAcquire失败就addWaiter入队然后再acquireQueued自旋或阻塞整个过程环环相扣。但具体的tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared这些方法默认是抛UnsupportedOperationException的强制子类去实现。这里我想重点强调一个容易忽略的设计点tryAcquire本身必须是线程安全的而且要确保在“可以获取”时才返回 true。因为调用它的线程可能不止一个大家同时CAS同一个state只有CAS成功的人才算抢到。你的tryAcquire里如果用了不安全的自增或者非原子的判断那AQS再牛也兜不住你的Bug。那什么场景需要自定义AQS呢常见的有几种你需要一把带超时、可中断、支持条件变量的自定义锁你需要一个带有特殊公平策略的同步门闩或者你想实现一个类似“同时最多N个线程通过”但规则比Semaphore更复杂的闸门。这些场景下继承AQS重写几个关键方法比你从零用synchronized和wait/notify拼一把锁要靠谱得多。4. 核心源码级机制拆解从抢锁到入队再到唤醒这一节我带着你从源码层面把AQS主流程走一遍。我不会逐行粘贴源码而是把最关键的执行流程和判断逻辑讲透。4.1 独占模式获取acquire的完整旅程假设现在有一个线程T1调用lock()最终会走到acquire(1)。这个方法的执行过程大致是先调用tryAcquire(1)尝试直接获取同步状态。这个方法是子类实现的。如果返回 true说明抢锁成功acquire方法直接返回线程T1进入临界区。如果tryAcquire返回 false说明抢锁失败此时调用addWaiter(Node.EXCLUSIVE)把当前线程包装成一个独占模式的节点挂到队列尾部。节点入队之后进入acquireQueued方法。这个方法里是一个死循环当前节点会不断检查自己的前驱节点是不是头节点。如果是就再试一次tryAcquire成功就说明轮到它了把自己设为头节点然后返回。如果前驱不是头节点或者抢锁还是失败就检查前驱节点的waitStatus决定是否要LockSupport.park阻塞自己。这里有个细节值得琢磨为什么当前节点要反复检查“前驱是不是头节点”因为头节点代表当前持有锁的线程或哨兵节点。只有头节点的下一个节点才拥有“被唤醒后再次尝试抢锁”的资格这就天然形成了一个FIFO顺序。在公平锁里这个顺序就是绝对公平的先入队的线程先获得锁。我早年看这段代码时一直有个疑问既然已经按FIFO排队了为什么唤醒之后不直接让自己持有锁还要再走一遍tryAcquire去CAS竞争原因很简单锁可能已经被释放了tryAcquire可以直接成功省掉一次阻塞唤醒同时这也保留了非公平锁的介入空间。对于公平锁来说tryAcquire里会额外检查队列里有没有比自己更靠前的节点有就直接返回 false把机会让给队首。4.2 入队与节点的状态流转addWaiter这个方法我觉得有必要多说两句。它做的事情是把新节点插到队列尾部但如果直接用CAS去更新尾节点在高竞争下CAS很容易失败。所以AQS的做法是首先尝试快速入队把当前尾节点作为前驱CAS将自己的节点设为新的尾节点成功就直接返回。如果CAS失败说明竞争激烈这时候进enq方法用死循环加上CAS保证一定能入队成功。enq里面有一个经典的处理如果队列为空尾节点为null先用CAS设置一个哨兵头节点然后再入队。这个哨兵头节点的引入让“判断队列是否为空”和“头节点的next是不是有效节点”这两个操作变得非常简单。入队成功后节点的waitStatus初始是0。之后在acquireQueued循环里会检查前驱节点的状态。如果前驱是SIGNAL状态说明前驱有义务在释放时唤醒自己那当前线程就可以放心地 park。如果前驱的waitStatus大于0说明前驱已经被取消就往前跳过取消的节点。如果前驱状态是0或者负值但不等于SIGNAL就尝试用CAS把前驱改成SIGNAL然后再次循环。这个过程说白了就是在彻底睡觉之前确保有人会在合适的时候叫你起床。这个“确保”是通过改变前驱节点的状态来完成的。4.3 独占模式释放release的唤醒链条释放过程的骨架在release(int arg)中核心逻辑如下调用tryRelease(arg)这是子类实现的方法。返回 true 表示锁已经真正释放比如ReentrantLock可重入计数减到了0。如果tryRelease返回 true就检查当前头节点的waitStatus。如果头节点状态不是0说明后面有等待线程需要被唤醒就调用unparkSuccessor去唤醒头节点的下一个有效节点。唤醒的线程从 park 状态恢复后会回到acquireQueued的死循环里再次尝试抢锁。这里有个我当初一直没想明白的细节unparkSuccessor在唤醒前会做一件事它先把头节点的waitStatus设置为0然后从头节点往后找到第一个waitStatus不是CANCELLED的节点再唤醒它。为什么要从头开始找因为尾节点和下一个节点的连接next指针在并发条件下可能失效而从头部往后遍历一定可以找到有效的后继节点。这种“宁可多遍历不依赖某个可能断裂的指针”的思想在并发代码里特别常见。4.4 共享模式与独占模式的本质差异共享模式和独占模式最大的区别在于独占锁同一时刻只能有一个线程持有而共享锁可以允许多个线程同时持有。体现在AQS的代码上共享模式下tryAcquireShared返回值的含义不一样。它返回的是一个 int负数表示获取失败。0 表示获取成功但后续的共享获取不会再成功。正数表示获取成功并且后续的共享获取还可能继续成功。为什么要注意这个返回值因为它直接决定释放时要不要唤醒后续的共享节点。setHeadAndPropagate这个方法的“propagate传播”部分就是当共享节点成功获取后如果发现剩余资源还够就继续唤醒后面的共享节点让它们也能依次通过。这就像一扇旋转门第一个人推门进去之后门还会继续转后面的人可以跟着进去直到门停下来。4.5 条件队列await与signal背后的机制如果你用过Condition你可能会好奇它跟AQS到底是什么关系。其实很简单ConditionObject是AQS的一个内部类每一个Condition对象内部维护着一个独立的条件等待队列。当你调用await()时当前线程会被封装成一个NodewaitStatus设为CONDITION挂到条件队列尾部然后释放当前持有的锁这里会调用你重写的tryRelease最后 park 自己。当你调用signal()时会从条件队列里取出第一个等待节点把它重新放回AQS的同步队列中然后将其状态从CONDITION改成0通过CAS把它从条件队列转移到同步队列。注意这里并不是直接唤醒它而是先让它进入同步队列排队等它真正排到队首时才有机会被唤醒抢锁。这个两段式的设计经常被误解我见过有人在signal之后立刻去访问共享变量结果还是读到旧值。原因就是signal并不保证await的线程立刻恢复执行它只是把那个线程重新放到了同步队列里真正恢复要等它抢到锁。5. JDK同步器如何借助AQS实现各自语义5.1 ReentrantLock可重入独占锁的教科书实现ReentrantLock是AQS最经典的子类。它的核心逻辑可以用一句话概括加锁时如果state为0则CAS置1如果state不为0且持有锁的线程是当前线程则state加1。释放锁时state减1减到0才真正释放。它内部还有公平锁和非公平锁两个实现区别只在tryAcquire里非公平锁直接 CAS不管队列里有没有人在等抢到就是赚到。这种“插队”行为虽然对队尾的线程不够友好但好处是能减少线程唤醒带来的上下文切换吞吐量通常更高。公平锁CAS前先检查hasQueuedPredecessors()如果队列里有比当前线程更早的等待节点就放弃这次竞争。成本是更频繁的线程切换但不会出现线程饿死。我在实际项目里很少用公平锁因为绝大多数场景不要求严格的公平性非公平锁那点“插队”带来的吞吐量提升更香。但如果你是做测试工具或者某些对公平性有硬性要求的场景公平锁就很有必要了。5.2 CountDownLatch共享锁与全量释放的配合CountDownLatch内部使用了AQS的共享模式。初始化时state设为计数值 N。每次countDown()调用tryReleaseShared对state减1当减到0时返回 true此时AQS会调用doReleaseShared把所有等待节点都唤醒。这里要注意的是CountDownLatch的计数器是一次性的减到0之后不能重置。如果你需要“能重置的倒计数门闩”标准库里没有直接提供但你可以基于AQS自己实现一个核心就是把 reset 操作封装成一次同步状态的重置并且保证在重置时没有线程正在等待。5.3 Semaphore共享模式下许可证的分发Semaphore里面state就是许可证数量。acquire时tryAcquireShared里做减法许可证够就CAS成功不够就返回负数线程入队等待。release时tryReleaseShared做加法唤醒等待线程。有一个比较容易踩的坑Semaphore的许可证是可以“透支”的。比如你把初始许可证设为0然后某个线程先release一次许可证就变成1了之后才有其他线程能acquire到。这种用法可以当成一个“手动置位的信号”来用但要小心不要让许可证数量因为多次release而过多导致并发控制失效。6. 基于AQS自定义同步器的完整实操案例这节我拿一个实际需求来演示怎么基于AQS写自定义同步器。6.1 需求背景与设计思路假设我需要在项目中实现一个“限流闸门”最多允许3个线程同时访问某个远程接口其他线程必须排队等待而且等待时支持超时超时后直接返回失败不无限阻塞。JDK里的Semaphore看起来能满足并发限制但不支持“超时返回失败”的定制语义它默认的是超时后抛异常或者返回false但行为细节和我要的不完全一样。所以我决定直接继承AQS用共享模式来实现这个闸门。6.2 核心代码实现import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.AbstractQueuedSynchronizer; public class TimeoutSemaphore { private final Sync sync; public TimeoutSemaphore(int permits) { sync new Sync(permits); } public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { long nanos unit.toNanos(timeout); return sync.tryAcquireSharedNanos(1, nanos); } public void release() { sync.releaseShared(1); } static final class Sync extends AbstractQueuedSynchronizer { Sync(int permits) { setState(permits); } Override protected int tryAcquireShared(int acquires) { for (;;) { int available getState(); int remaining available - acquires; if (remaining 0 || compareAndSetState(available, remaining)) { return remaining; } } } Override protected boolean tryReleaseShared(int releases) { for (;;) { int current getState(); int next current releases; if (next current) { throw new Error(许可证数量溢出); } if (compareAndSetState(current, next)) { return true; } } } } }这段代码里有几个地方需要重点解释。6.3 关键实现细节与参数计算第一tryAcquireShared返回的是剩余许可证数remaining。这个值无论正负都会被AQS的acquireSharedInterruptibly流程利用负数表示获取失败需要入队正数表示成功并且会触发传播机制让队列里后续的共享线程继续尝试。第二循环CAS的写法在tryAcquireShared里是必要的。因为这里的getState和compareAndSetState之间可能有其他线程修改了state如果用“先get再if再set”的非循环写法会出现ABA问题或者过度发放许可证的问题。CAS失败就重读最新值这是无锁编程的标准姿势。第三tryAcquireSharedNanos是AQS自带的带超时版本它内部处理了中断和超时两种情况。注意这里传入的是纳秒如果你外部传入的是毫秒一定要先转换成纳秒再传入否则超时时间会长达千倍。第四release时的next current检查是防止负数加法的溢出。极端情况下如果反复release上亿次int会溢出变成负数导致后续逻辑混乱。虽然实际中很难遇到但防御性编程成本很低写上不亏。6.4 实测效果与性能观察写完这个同步器之后我写了一个小Demo做验证开10个线程同时去抢3个许可证每次拿到许可证后模拟处理100毫秒处理完释放。观察到的结果是任意时刻最多只有3个线程在“处理中”其他线程全部阻塞在tryAcquire上。超时测试也符合预期把超时设为200毫秒而持证线程暂时不释放时后续请求会在200毫秒后返回 false没有出现线程泄漏。性能上这个基于AQS的实现比我自己最初用synchronizedwait/notify的写法好很多。原因在于synchronized的等待/唤醒机制在竞争激烈时会产生更多的线程状态切换而AQS的CLH队列让线程阻塞的顺序更可控加上CAS在低竞争时几乎无开销吞吐量自然就上来了。7. 自查手册AQS使用与理解中的高频雷区7.1 await/signal 用错位置导致死锁这是我见过新手犯得最多的问题在持有锁的临界区里调用await()但忘了await会自动释放锁或者在不该释放锁的地方调用了signal()导致唤醒的线程拿到锁之后发现条件不满足又回去睡了。你需要记住的规则其实就两条await()只能在持有锁时调用它内部会释放锁线程被signal后重新抢到锁才会返回返回时锁的状态和调用await前一致。signal()不一定非得在持有锁时调用但如果你的业务逻辑依赖条件变量来判断是否signal那就有潜在竞态可能条件已经满足但signal发生在await之前导致等待线程永远等不到唤醒。7.2 重写tryAcquire时没有正确理解返回值的含义tryAcquire返回 true/false这个比较简单。但共享模式下的tryAcquireShared返回的是一个int很多人只记得“负数失败、非负成功”却忽略了“0和正数”的差异。如果你返回0后续共享节点不会被传播唤醒这可能导致明明还剩下资源但后续线程却阻塞住了。所以我建议在拿不准的时候让tryAcquireShared返回剩余的可用数量而不是简单返回1。7.3 state初始值与实际语义不一致写自定义同步器时setState的初始值一定要想清楚。如果你要限制并发数为NsetState(N)如果你要实现一个“一开始就锁住等某个条件满足才放行”的门闩setState(0)然后其他线程调用tryAcquireShared时检查条件再唤醒。这里最容易犯的错误是搞反了0和N的含义尤其从ReentrantLock的思维切到共享模式时特别容易转不过来。7.4 忽略中断处理导致线程卡死AQS的acquireInterruptibly方法响应中断但如果你直接调acquire则不会响应线程中断。很多人在自定义同步器里图省事直接调了acquire结果线程取消时没法从等待中出来只能干等锁释放。如果线程可能被外部取消请一定使用带Interruptibly的方法。8. 工具选型解析什么时候用AQS什么时候用更高层的封装你可能会有个疑问既然AQS这么强大为什么我平时写代码几乎不需要直接碰它答案很简单Java并发包已经把绝大多数常见场景封装好了。你遇到“互斥锁”的需求用ReentrantLock遇到“多个线程等一个信号”的需求用CountDownLatch遇到“限流”的需求用Semaphore遇到“读写分离”的需求用ReentrantReadWriteLock。这些工具都是基于AQS实现的你在使用它们的时候其实已经在间接享受AQS带来的收益。那什么情况下才值得自己继承AQS写同步器呢我总结了几类场景你需要一个JDK没有的同步语义比如“可重置的倒计数门闩”、“支持批量获取许可证的信号量”、“带优先级的等待队列”。你需要对线程排队行为做深度定制比如限制队尾等待时间、动态调整并发度。你正在做一些底层的框架类组件希望减少对象层级和调用开销。在这些场景之外我建议你优先使用现成的并发工具类。这不是“会不会用AQS”的问题而是工程上“用最合适的轮子”的问题。AQS更多时候是你在阅读框架源码、排查诡异的并发问题时必须掌握的底层知识。9. 性能表现与调优方向9.1 AQS的性能特征在哪里AQS的性能优势主要体现在三个地方第一无竞争时开销极低。一个线程抢锁就是一次 CAS几十纳秒就完成了比synchronized在无竞争时的偏向锁路径还要简洁。第二轻度竞争时避免立刻阻塞。AQS给线程设计了一个抢夺窗口在进入队列之前就有机会通过CAS直接抢到锁减少了线程从用户态到内核态切换的开销。第三排队阻塞唤醒的顺序可控。CLH队列保证了唤醒顺序接近FIFO减少了“惊群”现象——每次只唤醒下一个需要唤醒的节点而不是像某些实现那样一口气唤醒所有等待线程。9.2 实战中的调优细节如果你用AQS实现自定义同步器有几个调优细节值得关注减少临界区长度。AQS只是帮你管好了“谁有资格进临界区”但如果你在临界区里做了很重的IO操作锁被长时间持有其他线程都得等着AQS再高效也救不了你。合理选择自旋次数。AQS本身已经做了一层有限的自旋但如果你能预估自己的临界区极短可以考虑在tryAcquire里增加几次CAS重试再返回false减少线程入队和阻塞的频率。注意CPU核数。在多核机器上“自旋抢锁”更划算单核机器上自旋只会浪费CPUAQS的acquireQueued会自动尝试两轮后 park但如果你的线程特别多可以在上层做限制避免大量线程同时进入等待队列。我在跑测试的时候曾经发现当等待线程数量从8增加到64时AQS的吞吐量下降了将近一半。这不是AQS的Bug而是线程上下文切换的开销上来了。这时候与其优化AQS不如减少进入临界区的线程数量比如用有界队列加拒绝策略把多余的请求挡在外面。10. 条件变量Condition的进阶用法与注意事项10.1 Condition与AQS条件队列的关系再梳理Condition对象是通过lock.newCondition()创建的它内部就是AQS的ConditionObject。每个Condition实例都有独立的条件等待队列。这就意味着一个锁可以创建多个Condition比如一个“不满”条件、一个“不空”条件让生产者线程和消费者线程各自等待各自的条件。这种多条件队列的能力是synchronized的wait/notify做不到的。synchronized只有一个隐式条件队列你没法区分“等满”的消费者和“等空”的生产者只能用一个条件变量硬扛结果就是每次notifyAll都会唤醒所有线程然后大部分线程发现自己条件不满足继续睡白白浪费上下文切换。10.2 必踩的坑signal后不立即执行与锁的释放时机我再强调一次signal()只负责把节点从条件队列移到同步队列它不等于唤醒线程立即运行。被signal的线程必须重新竞争到锁才能真正从await中返回。所以如果你写了这种代码性能和行为都会有隐患lock.lock(); try { condition.signal(); // 先唤醒 sharedValue 1; // 再修改共享变量 } finally { lock.unlock(); }因为signal后被唤醒的线程还没有真正拿到锁它读到sharedValue的时间是不可控的。你可能习惯性地认为“signal 之后等待线程就马上醒了”实际上它醒来后还得抢锁抢到锁才能回到await调用点去读共享变量。所以正确的做法是先修改共享变量再signal最后释放锁。10.3 await中线程中断的正确处理await方法会抛出InterruptedException。如果你在await里被中断线程会从等待队列中移除不再等待条件成立而是直接抛出异常返回。这意味着你的业务代码必须处理“被中断时条件还没成立”的情况不能抱着“反正条件会满足”的侥幸心理。常见的处理方式是把你需要等待条件满足的逻辑放进一个while循环里即使线程被意外唤醒或者被中断循环会重新检查条件。如果条件不成立就继续await。这个模式是Condition使用的“黄金法则”永远在循环中等待不要在if中等待。11. 我的实测心得与建议AQS这个东西光看源码觉得抽象但一旦你亲手实现过一个自定义同步器再回头看ReentrantLock的源码那种“豁然开朗”的感觉是非常强烈的。我个人学习AQS的路径是先通过画图把“线程抢锁、入队、阻塞、唤醒”的完整流程梳理出来然后去读ReentrantLock的源码对照验证最后自己写一个共享模式的自定义同步器来理解tryAcquireShared的返回值语义。这里分享一个我很受用的思路把AQS想象成一个“线程调度器”你重写的tryAcquire和tryRelease只是决定“谁能通过”的规则卡片而排队的秩序、阻塞的控制、唤醒的时机调度器全包了。一旦你建立了这种分工意识很多代码细节就变得顺理成章了。如果你打算在项目里真正使用自定义AQS同步器我建议一定先写并发压测用例覆盖“竞争激烈、线程中断、超时返回、大量线程同时释放”这些极端场景。AQS本身实现得非常严谨但你的子类实现如果考虑不周很容易在边界条件下翻车。而且这类Bug复现难度很高往往要压测到几十万次才偶然出现排查起来非常痛苦。我自己在项目中实现过一个“按key维度限流”的同步器同一个key最多允许N个线程并发不同key之间互不影响。实现方式是在AQS内部维护一个ConcurrentHashMapString, Sync每个key对应一个Sync实例。这个方案跑了一年多线上没有出现过死锁或者超时失效的问题。这个例子也从侧面印证了AQS向上封装的灵活性只要你想清楚同步语义AQS能把最底层的排队等待机制帮你扛住让你专注于业务规则本身。最后再提醒一点AQS是一个相对底层的并发基础设施它的性能和正确性依赖你对并发模型的理解。如果你对volatile、CAS、线程状态切换这些基础概念还不够熟悉建议先去补一补这些前置知识再回来啃AQS会轻松很多。并发编程没有银弹AQS也不是万能钥匙但它是你理解Java并发世界绕不开的那扇门。把这扇门推开后面的路会越走越宽。
延伸阅读

更多相关文章

2026/10/17 13:10:26

线性混合模型LMM入门:嵌套数据与重复测量的最佳分析方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/17 13:05:25

Codex 实战:从需求到交付,把 AI 当队友而非工具

Codex 出来这么久,各路实战教程我也翻了不少,但发现绝大多数人的用法还停留在“让它写个函数”“补全几行代码”的层面。这篇我不打算再重复那些 API 文档里就有的东西,而是把过去半年把 Codex 当“队友”而不是“工具”用的实操经验完整复盘…

2026/10/17 14:20:30

2022国赛B题纯方位无源定位:从圆形到锥形编队的建模与MATLAB实现

简介:本资源为2022年全国大学生数学建模竞赛本科组B题的完整论文PDF,聚焦无人机纯方位无源定位问题,适合备战数模竞赛的本科生及指导教师参考。论文围绕圆形与锥形编队,建立被动接收信号无人机的定位模型与位置调整模型&#xff0…

2026/10/17 14:20:30

uniapp跨端开发指南:从环境搭建到性能优化

简介:这是一份《uniapp从入门到精通》完整教程文档,专为希望掌握uni-app跨平台开发的初中级开发者打造。文档从环境搭建起步,涵盖HBuilderX与微信开发者工具的配置、项目创建与多端运行,并深入项目目录结构、开发规范、pages.json…

2026/10/17 14:20:30

AI Agent工具接入为何总翻车?MCP协议实战拆解与标准化方案

先给结论:AI Agent接上工具之后依然难用,问题通常不是模型不够聪明,而是工具这一层太乱。我过去带过好几个工具接入项目,最常见的状态是:OpenAPI文档塞给Agent,函数调用也配了,Demo能跑&#xf…

2026/10/17 14:20:30

AI辅助论文写作全流程实战:选题、框架到降重的人机协作指南

帮人改了三年论文,自己也写过不少稿子,我现在越来越离不开一个习惯:先把思路交给AI捋顺,再动手写正文。这不是偷懒,而是把最耗精力的“从零到一”交给工具,把判断、取舍和深度打磨留给自己。书匠策AI就是我…

2026/10/17 14:20:30

SFML图形绘制黑屏排查与渲染原理深度解析

1. 为什么“绘制图形”是SFML入门真正的分水岭很多人学SFML,一开始被窗口创建、事件循环这些基础操作吸引,觉得“哦,能弹个黑窗出来了”,就以为自己上道了。但真正卡住90%初学者的,从来不是怎么开窗,而是—…

2026/10/17 14:15:30

JavaScript PDF下载兼容方案:覆盖iOS Safari、微信、华为等全平台

1. 项目概述:为什么一个PDF下载功能要专门写一篇长文?“js 通过URL下载PDF文件(兼容PC端、移动端大部分浏览器)”——这行字看起来简单,但我在过去三年里,光是为这个需求就重构了5次前端下载逻辑。不是因为…

2026/10/16 5:33:43

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/16 4:03:48

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/17 14:20:58

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/17 0:04:32

PlanarSLAM:面向曼哈顿世界的点线面联合SLAM系统

简介:本资源是面向机器人视觉与SLAM研究者的C开源RGB-D SLAM系统实现,专为结构化室内场景(如办公室、走廊)设计,通过融合点、线、平面三类几何特征并引入“曼哈顿世界”假设,显著提升位姿跟踪鲁棒性与稠密地…

2026/10/17 0:04:32

TDH Workflow调度实战:从DAG依赖驱动到任务编排与故障排查

1. 从一个调度脚本说起:TDH Workflow到底在解决什么问题第一次接触TDH的Workflow,是在一个数据清洗项目里。当时团队每天要跑几十个ETL任务,任务之间有严格的先后依赖——A表清洗完才能跑B表的关联,B表跑完才能触发C表的聚合。最开…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑