Java高并发底层原理(十九)—— AQS 独占与共享模式如何完成排队与唤醒

📅 2026/7/13 12:59:50
Java高并发底层原理(十九)—— AQS 独占与共享模式如何完成排队与唤醒
1. AQS 先解决什么问题AQS全名是AbstractQueuedSynchronizer。它本身不是一把具体的锁而是一套用于构建同步器的基础框架。ReentrantLock、Semaphore、CountDownLatch等工具可以基于 AQS 实现不同语义但它们都会遇到同一个底层问题当多个线程同时竞争同一份资源时成功的线程继续执行失败的线程不能一直空转抢资源而要进入等待队列并在合适的时候被唤醒。本文先从独占模式讲起也就是同一时刻只有一个线程能够获取资源的场景再在独占模式的基础上补充共享模式。以ReentrantLock为例lock()最终会进入 AQS 的独占获取流程unlock()最终会进入 AQS 的独占释放流程。AQS 在中间承担的职责不是决定“什么叫锁可用”而是把获取失败的线程组织成队列再用park / unpark完成阻塞和唤醒。2. state 只是同步状态具体语义由子类决定AQS 内部维护一个int state它表示同步状态但这个状态的具体含义并不固定。对ReentrantLock来说state 0可以表示锁空闲state 0可以表示锁已经被某个线程占用并且数值还能表示重入次数对Semaphore来说state可以表示剩余许可证数量。因此AQS 不能把获取和释放规则写死。它只提供统一流程具体规则交给子类实现tryAcquire()判断当前线程能否获取资源并在成功时修改statetryRelease()负责释放资源并告诉 AQS 资源是否已经真正释放干净。方法作用谁来实现规则tryAcquire(int arg)尝试获取资源子类tryRelease(int arg)尝试释放资源子类acquire(int arg)获取失败后入队等待AQSrelease(int arg)释放成功后唤醒后继AQS以ReentrantLock为例线程第一次加锁可以把state从0改成1同一线程重入时state继续增加释放时state逐步减少。只有当state减到0时锁才算真正释放AQS 才需要考虑唤醒等待队列里的后继线程。3. acquire 的第一步不是排队而是先尝试获取独占模式的获取入口可以简化成下面这样publicfinalvoidacquire(intarg){if(!tryAcquire(arg)){acquireQueued(addWaiter(Node.EXCLUSIVE),arg);}}这段代码体现了 AQS 获取资源的基本顺序当前线程先直接调用tryAcquire()尝试一次如果成功就不需要进入等待队列如果失败才会把当前线程包装成节点并加入队列。这里要注意AQS 不会一上来就让线程排队因为资源可能正好空闲。只有当子类的tryAcquire()明确返回失败时AQS 才接管后续的排队和阻塞流程。4. 获取失败后线程要包装成 Node 入队等待队列里保存的不是裸线程而是一个个Node。因为排队不仅要知道哪个线程在等还要记录它在队列中的前后关系以及它和前驱节点之间的唤醒约定。本文沿用 JDK 8 AQS 源码中的命名核心字段可以先简化为staticfinalclassNode{volatileintwaitStatus;volatileNodeprev;volatileNodenext;volatileThreadthread;}AQS 的等待队列是一个双向链表。获取失败的线程会被包装成Node然后通过 CAS 插入队尾。队列第一次初始化时会先创建一个不代表具体等待线程的 dummy head真正等待的节点接在它后面。head不是普通等待节点而是队列已经推进到的位置。一个节点只有在自己的前驱是head时才说明它排在最前面才有资格尝试获取资源。这个规则使 AQS 不需要让所有等待线程一起抢锁而是尽量按照队列顺序推进。5. addWaiter 和 enq安全地插入队尾addWaiter(Node.EXCLUSIVE)负责把当前线程包装成独占模式节点并尝试插入队尾。队列已经初始化时它会先走快速路径读取当前tail把新节点的prev指向旧tail再通过 CAS 把tail改成新节点。CAS 成功后当前节点才真正抢到队尾位置随后再补上旧尾节点的next指针。如果队列还没有初始化或者多个线程同时入队导致 CAS 失败就会进入enq(node)。enq()是兜底入队逻辑如果发现tail null先创建 dummy head如果队列已经存在就在循环中反复尝试 CAS 更新tail直到当前节点成功入队。这里需要注意一个细节第一次进入enq()时可能只是完成队列初始化并不会立刻把当前节点插入进去下一轮循环看到tail ! null后才会继续执行真正的队尾插入。6. acquireQueued入队后循环等待节点入队后会进入acquireQueued()。它的核心不是“直接睡眠”而是在循环中反复判断自己是否已经排到最前面以及当前资源是否已经可用。简化后的结构如下for(;;){Nodepnode.predecessor();if(pheadtryAcquire(arg)){setHead(node);p.nextnull;return;}if(shouldParkAfterFailedAcquire(p,node)){parkAndCheckInterrupt();}}每一轮都要重新读取前驱节点因为等待队列不是静态的。前驱节点可能已经取消等待并被跳过前驱节点的waitStatus也可能从普通状态变成SIGNAL。当前节点不能缓存旧判断而要在每一轮重新确认自己现在排在谁后面前驱是不是head前驱是否还能承担唤醒后继的责任。其中p head只表示当前节点有资格尝试获取资源不表示它已经拿到了资源。真正能否继续执行还要看tryAcquire(arg)是否成功。成功之后当前节点会通过setHead(node)变成新的head原来的旧head会断开next指针方便后续回收。7. SIGNALpark 前必须先建立唤醒关系如果当前节点没有成功获取资源就不能马上park()。在 AQS 中线程睡下去之前必须先确保将来有人负责叫醒它。这个约定通过前驱节点的waitStatus表达其中最重要的是SIGNAL。waitStatus值含义SIGNAL-1当前节点的后继节点需要被唤醒CANCELLED1当前节点已经取消等待00普通初始状态SIGNAL是标在前驱节点上的不是标在当前节点自己身上的。假设Node C准备阻塞它会先尝试把前驱Node B的状态改成SIGNAL意思是B后面有节点要睡了等B释放资源或队列推进时要负责唤醒后继。因此shouldParkAfterFailedAcquire(p, node)的关键逻辑可以压缩成三种情况如果前驱已经是SIGNAL说明唤醒关系已经建立当前线程可以安全地park如果前驱状态是0先用 CAS 把前驱改成SIGNAL本轮不睡回到循环重新判断如果前驱状态大于0说明前驱已经取消等待当前节点要跳过它重新接到有效前驱后面。前驱状态当前节点的处理本轮是否 parkSIGNAL唤醒关系已经建立是0尝试把前驱改成SIGNAL否 0跳过取消节点修正队列关系否这也是为什么设置完SIGNAL后当前线程通常不会立刻睡下去而是回到外层循环重新判断一次。因为在并发环境里前驱可能已经变化资源也可能已经释放AQS 不会让线程在没有重新确认局势的情况下直接阻塞。8. park / unpark 只是挂起和唤醒不等于交接锁当shouldParkAfterFailedAcquire()返回true后当前线程才会通过LockSupport.park()挂起。这里的阻塞不是synchronized竞争失败时的BLOCKED也不是Object.wait()而是 AQS 基于LockSupport使用的park / unpark机制。park的含义是让当前线程暂时挂起不再占用 CPU 空转unpark的含义是让被挂起的线程有机会醒来。被唤醒的线程不会直接获得锁而是回到acquireQueued()的循环中重新判断自己的前驱是否为head再调用tryAcquire()。这一区分很重要AQS 的释放线程只负责唤醒后继真正获取资源的动作仍然发生在被唤醒线程自己的tryAcquire()中。尤其在非公平锁中新来的线程也可能在这个间隙竞争成功所以被unpark只能说明线程获得了重新尝试的机会不能说明锁已经交到它手里。9. release释放资源后才考虑唤醒后继独占模式的释放入口可以简化成publicfinalbooleanrelease(intarg){if(tryRelease(arg)){Nodehhead;if(h!nullh.waitStatus!0){unparkSuccessor(h);}returntrue;}returnfalse;}和获取流程对应AQS 释放时也先把具体规则交给子类。tryRelease(arg)负责修改state并返回资源是否真正释放成功。仍以ReentrantLock为例如果当前线程重入了两次第一次unlock()只会让state从2变成1锁还没有真正释放只有第二次unlock()让state从1变成0tryRelease()才会返回true。所以release()不是一调用就唤醒后继。只有tryRelease()明确表示资源已经释放干净AQS 才会从head出发检查等待队列中是否存在需要唤醒的节点。10. unparkSuccessor清理 head 状态再唤醒有效后继当release()发现head存在并且head.waitStatus ! 0时会调用unparkSuccessor(head)。这里不是直接唤醒head.next而是先处理head自己的状态。如果head.waitStatus 0通常也就是SIGNALAQS 会先尝试把它改回0。因为SIGNAL表示前驱节点上有一个“需要唤醒后继”的提醒既然释放流程已经开始处理这个提醒就要先把标记复位再去唤醒后继节点。之后AQS 会寻找head后面的第一个有效等待节点。理想情况下这个节点就是head.next但如果head.next已经取消等待或者在并发入队过程中next链还没有完全接好AQS 会从tail往前反向查找跳过waitStatus 0的取消节点找到真正还在等待的节点并unpark它。这一步可以概括为unparkSuccessor(head)先清理head上已经被处理的唤醒标记再从队列中找到有效后继并唤醒。被唤醒的线程随后回到自己的获取循环重新尝试tryAcquire()。11. 取消节点不会继续参与唤醒链路等待过程中线程可能因为中断、超时等原因取消等待。取消后的节点会被标记为CANCELLED也就是waitStatus 0。这种节点已经不再参与后续竞争也不应该继续承担唤醒后继的职责。当前节点在检查前驱时如果发现前驱已经取消就会向前跳过这些无效节点重新接到一个有效前驱后面释放线程在唤醒后继时也会跳过取消节点避免把唤醒机会浪费在已经放弃等待的线程上。取消节点的清理不是必须在某一个时刻一次性完成。AQS 更像是在入队、阻塞判断、释放唤醒等过程中顺手修正队列关系。只要后续流程能够跳过无效节点并保证有效等待节点还能被唤醒队列就可以继续向前推进。12. acquire 和 release 如何连成一条链现在可以把独占模式的获取和释放放到同一条链路里看。线程调用lock()后先由子类的tryAcquire()判断能否直接获取资源失败后AQS 把当前线程包装成Node插入队尾并在acquireQueued()中循环等待。等待线程不是一失败就睡而是先确认自己是否排到head后面如果还不能获取就通过前驱的SIGNAL建立唤醒关系然后才park。释放方向则相反。线程调用unlock()后子类的tryRelease()先修改state并判断资源是否真正释放如果释放成功AQS 从head出发清理已经处理的SIGNAL标记找到有效后继节点并unpark。后继线程醒来后并不会直接拥有锁而是继续回到获取循环重新判断资格并调用tryAcquire()。成功之后它成为新的head队列向前推进一格。这一整套机制的关键不在于某一个方法而在于几个动作之间的配合tryAcquire()决定能不能拿资源Node队列保存等待顺序SIGNAL保证线程睡前有人负责唤醒park / unpark负责挂起和叫醒tryRelease()决定资源是否真正释放setHead()则表示队列已经推进到新的位置。13. 共享模式复用同一条同步队列独占模式解决的是“同一时刻只能有一个线程通过”的问题。共享模式要解决的是另一类问题资源可能允许多个线程同时通过。比如Semaphore可以有多个许可证CountDownLatch在计数归零后可以让所有等待线程继续执行。共享模式没有重新设计一条等待队列而是复用前文已经讲过的 AQS 同步队列。区别在于入队节点的模式从独占变成共享获取和释放入口也换成了共享版本。模式获取入口释放入口子类规则独占模式acquire()release()tryAcquire()/tryRelease()共享模式acquireShared()releaseShared()tryAcquireShared()/tryReleaseShared()因此前文关于Node、head、SIGNAL、park / unpark的主线仍然成立。共享模式新增的问题不是“线程还要不要入队”而是当一个共享节点成功获取资源后后面的共享节点是否也应该继续被唤醒。14. tryAcquireShared 为什么返回 int独占模式的tryAcquire()返回boolean因为它只需要回答一个问题当前线程是否获取成功。共享模式的tryAcquireShared()返回int因为它除了要表示成功或失败还要告诉 AQS 是否需要继续传播唤醒。返回值简化含义 0获取失败需要入队等待 0获取成功但后续共享节点不一定还能继续获取 0获取成功并且后续共享节点可能也能继续获取以Semaphore为例state可以表示剩余许可证。线程获取一个许可证后如果剩余许可证仍然大于0说明后面的共享节点也可能继续通过如果刚好变成0当前线程虽然获取成功但后面线程暂时不能继续获取。CountDownLatch的语义又不一样。等待线程调用await()时如果state还没有归零tryAcquireShared()返回负数线程进入同步队列等待一旦state 0等待线程获取共享资源成功并且后面的等待线程也都应该被放行。这里要抓住一个差异独占模式只需要唤醒一个后继去竞争共享模式可能需要沿着队列继续唤醒多个后继让“共享可通过”的状态向后传播。15. doAcquireShared共享节点成功后还要传播共享模式获取失败后也会包装成共享节点入队然后在队列中循环等待。这个过程和独占模式相似当前节点仍然要等到自己的前驱是head才有资格调用tryAcquireShared()。不同点出现在获取成功之后。独占模式获取成功后当前节点只需要setHead(node)表示队列推进到当前位置。共享模式获取成功后还会调用类似setHeadAndPropagate(node, result)的逻辑当前节点成为新的head同时根据tryAcquireShared()的返回值和队列状态决定是否继续唤醒后面的共享节点。这就是共享模式和独占模式最核心的区别独占模式强调“一个线程成功后其他线程继续等待”共享模式强调“一个共享节点成功后可能要继续把唤醒机会传给后面的共享节点”。16. releaseShared共享释放为什么需要继续传播共享模式的释放入口可以简化成publicfinalbooleanreleaseShared(intarg){if(tryReleaseShared(arg)){doReleaseShared();returntrue;}returnfalse;}这里仍然是先调用子类方法。tryReleaseShared()负责修改state并告诉 AQS 这次释放是否可能让等待队列里的共享节点继续执行。比如CountDownLatch的countDown()会减少state只有当计数减到0时等待线程才应该被唤醒在计数还没有归零之前即使调用了countDown()也不能放行等待线程。当tryReleaseShared()返回true后AQS 会进入doReleaseShared()。它和独占模式中的unparkSuccessor(head)有相似之处都会从head出发处理SIGNAL并唤醒有效后继。不同之处在于共享模式要保证唤醒动作能够继续传播所以会引入一个额外状态PROPAGATE。waitStatus值在本文中的作用SIGNAL-1后继节点需要被唤醒PROPAGATE-3共享模式下需要继续传播唤醒CANCELLED1节点已经取消等待00普通初始状态PROPAGATE可以先理解成共享模式中的传播标记。它不是为了表示当前节点自己要被唤醒而是为了保证共享释放或共享获取成功后后面的共享节点不会因为某个瞬间没有SIGNAL标记而断掉传播链路。17. Semaphore 和 CountDownLatch 如何落到共享模式共享模式的抽象比较绕可以用两个工具把它落回state。Semaphore的state表示剩余许可证。线程获取许可证时tryAcquireShared()会尝试扣减state如果扣减后结果小于0表示许可证不足当前线程需要入队等待如果扣减成功当前线程可以继续执行如果扣减后还有剩余许可证AQS 还有机会继续唤醒后面的共享节点。CountDownLatch的state表示计数。等待线程调用await()时并不是去减少state而是检查计数是否已经归零如果没有归零等待线程进入同步队列如果已经归零等待线程直接通过。其他线程调用countDown()时才会减少state当最后一次减少让state变成0releaseShared()会触发共享唤醒把等待队列里的线程陆续放出来。工具state含义共享获取什么时候成功共享释放什么时候传播Semaphore剩余许可证成功扣减许可证释放许可证后可能唤醒等待者CountDownLatch剩余计数计数已经归零最后一次countDown()归零时传播这样看共享模式并不是一种固定业务语义而是一套“允许多个线程通过时如何排队、唤醒和继续传播”的通用机制。Semaphore和CountDownLatch都使用共享模式但一个围绕许可证数量变化一个围绕倒计时是否归零。18. 公平锁和非公平锁差在哪里前面独占模式已经说明获取失败的线程会进入 AQS 同步队列后续由前驱释放时唤醒。公平锁和非公平锁没有改变这条队列它们的区别只发生在新线程刚来抢锁时是否允许它绕过已经排队的线程。非公平锁会先直接尝试一次 CASif(compareAndSetState(0,1)){setExclusiveOwnerThread(Thread.currentThread());}else{acquire(1);}因此即使同步队列里已经有线程在等待只要新线程刚好看到state 0并 CAS 成功它就可以直接获取锁。公平锁则会在 CAS 前多做一次队列检查if(!hasQueuedPredecessors()compareAndSetState(0,1)){setExclusiveOwnerThread(current);returntrue;}hasQueuedPredecessors()可以先理解为当前线程前面是否已经有等待者。如果前面有人公平锁不会让新线程直接抢而是让它进入同步队列。类型获取策略特点非公平锁新线程先 CAS 抢一次失败再排队吞吐通常更高但可能插队公平锁先检查队列前面没人才能抢更尊重等待顺序但吞吐可能下降所以“公平”不是线程调度层面的绝对公平而是在 AQS 队列层面尽量避免新来的线程绕过旧线程。普通业务场景下ReentrantLock默认使用非公平锁只有当业务特别关心等待顺序或饥饿风险时才更倾向使用公平锁。19. Condition从“等锁”扩展到“等条件”AQS 同步队列解决的是“线程想获取锁但暂时获取不到”的问题。Condition解决的是另一个问题线程已经拿到锁但发现业务条件不满足需要暂时等待。以阻塞队列中的消费者为例lock.lock();try{while(queue.isEmpty()){notEmpty.await();}Objectitemqueue.remove();}finally{lock.unlock();}消费者执行到await()时说明它已经持有锁。它不是抢锁失败而是发现队列为空业务条件不满足。此时它不能继续占着锁等待否则生产者无法获得同一把锁也就无法放入元素。因此await()必须先让当前线程进入 Condition 条件队列再释放锁让其他线程有机会修改条件。Condition 会在 AQS 同步队列之外维护一条条件队列。两条队列的等待原因不同队列等待原因AQS 同步队列想获取锁但暂时获取不到Condition 条件队列已经持有锁但业务条件不满足当消费者调用await()后它会进入 Condition 条件队列并释放当前持有的锁。此时可以理解成此时Node(C1)和Node(C2)等的是业务条件被signal()而Node(B)等的是重新获取锁。两条队列虽然都保存等待线程但等待原因不同。当其他线程修改条件后调用signal()并不是让C1立刻继续执行而是把C1从 Condition 条件队列转移到 AQS 同步队列所以signal()的准确含义是条件可能已经变化先把一个条件等待节点转回同步队列。至于这个线程什么时候从await()返回还要等当前持锁线程unlock()然后它在 AQS 同步队列中重新竞争锁。因为调用signal()的线程此时通常还持有锁被转移的线程不能马上从await()返回只能先进入同步队列等当前线程unlock()后再重新竞争锁。只有重新获取锁成功await()才真正返回。因此Condition 的完整链路可以压缩成一句话await()让线程从“持锁执行”进入“条件等待”并释放锁signal()只负责把它从条件队列转回同步队列真正继续执行还要等它重新获得锁。这也解释了为什么await()外面必须用while而不是if。线程从await()返回只能说明它重新拿到了锁不能说明业务条件一定仍然满足。条件可能已经被其他线程改变也可能只是被唤醒后重新竞争到了锁所以必须重新检查lock.lock();try{while(!conditionSatisfied()){condition.await();}// 条件真正满足后再执行}finally{lock.unlock();}signal()和signalAll()的区别也可以放在这个模型里理解前者只转移一个条件节点后者转移全部条件节点。它们都只是让等待线程回到 AQS 同步队列而不是直接继续执行。方法动作适合场景signal()转移一个等待节点一次状态变化通常只满足一个等待者signalAll()转移所有等待节点状态变化可能影响多个等待者或无法确定该唤醒谁到这里AQS 的等待模型就从“等锁”扩展成了“先等条件再等锁”。20. 中断不同等待方式的响应差异中断不会改变 AQS 的队列模型它只影响线程在等待期间是否可以提前退出。这里主要区分三种方式普通lock()、lockInterruptibly()和Condition.await()。方法等待期间被中断结果lock()可以被唤醒但不退出等锁继续排队最终拿到锁后恢复中断标记不抛异常lockInterruptibly()响应中断退出等锁直接抛出InterruptedExceptionCondition.await()响应中断退出条件等待但要重新拿到锁后才抛出InterruptedException普通lock()是不可中断获取锁所以它不会抛出InterruptedException外层也不能用catch (InterruptedException e)捕获它lock.lock();try{// 临界区}finally{lock.unlock();}如果业务希望线程在等锁阶段就能被取消应使用lockInterruptibly()。它在获取锁期间被中断时会直接抛出InterruptedException。注意只有成功拿到锁以后才应该执行unlock()try{lock.lockInterruptibly();try{// 临界区}finally{lock.unlock();}}catch(InterruptedExceptione){Thread.currentThread().interrupt();// 处理中断例如返回、取消任务或向上结束流程}Condition.await()也是可中断等待但它响应的是“条件等待阶段”的中断。线程在await()中被中断后需要先重新获取锁然后才把InterruptedException抛给调用代码所以finally中仍然可以安全释放锁lock.lock();try{while(!conditionSatisfied()){condition.await();}// 条件真正满足后再执行}catch(InterruptedExceptione){Thread.currentThread().interrupt();// 处理中断例如返回、取消任务或向上结束流程}finally{lock.unlock();}所以可以概括成一句话lock()不抛中断异常lockInterruptibly()在等锁阶段就能抛await()能响应条件等待期间的中断但异常要等重新拿到锁后才交给调用代码。总结AQS 的因果链可以从state不可用开始理解独占模式下因为同一时刻只能有一个线程通过所以获取失败的线程进入同步队列并通过SIGNAL、park / unpark建立可靠的睡眠和唤醒关系共享模式在此基础上增加传播问题当一个线程成功通过后后面的共享线程也可能继续通过所以需要把共享可用状态继续向后传递。公平锁和非公平锁没有改变同步队列只是在新线程进入竞争时改变策略非公平锁允许先抢一次公平锁则先检查队列中是否已有等待者。Condition 又补充了另一类等待原因线程不是没抢到锁而是拿到锁后发现条件不满足于是先进入条件队列并释放锁等signal()转回同步队列后再重新竞争锁。最终AQS 仍然只提供通用机制用state表示状态用队列保存等待顺序用阻塞和唤醒恢复线程具体语义则由锁、信号量、倒计时器或条件等待来定义。