AQS 原理:Java 并发同步器的核心骨架

📅 2026/7/29 23:38:03
AQS 原理:Java 并发同步器的核心骨架
AQS 原理Java 并发同步器的核心骨架目录从 ReentrantLock 到 AQSAQS 的设计思想state同步状态的核心变量CLH 队列线程排队的底层结构独占模式的完整获取与释放共享模式Semaphore 和 CountDownLatch 怎么用 AQSConditionAQS 里的第二条队列小结从 ReentrantLock 到 AQSReentrantLock 是一把可重入的互斥锁Semaphore 是一个信号量CountDownLatch 是一个倒计数门闩ReadWriteLock 是一把读写分离的锁。它们的功能不同API 不同但在 java.util.concurrent 包里它们的底层实现共享同一套骨架——AQSAbstractQueuedSynchronizer。这些同步工具做的事情可以抽象成两个问题同步状态怎么表达竞争失败的线程怎么排队。AQS 把这两个问题的通用解法封装成框架子类只需要定义state 代表什么和怎么获取/释放它线程排队、阻塞、唤醒这些事情全部交给 AQS。所以理解 AQS就等于理解了 JUC 并发工具的共同基础。AQS 的设计思想AQS 的核心设计可以拆成三个要素状态 队列 模板方法。状态用一个 volatile int state 表达同步资源的情况。不同同步器对 state 赋予不同含义AQS 不规定它代表什么只提供 getState、setState、compareAndSetState 三个操作方法。队列竞争失败的线程进入一个 FIFO 双向链表排队通过 park/unpark 阻塞和唤醒不靠自旋消耗 CPU。模板方法AQS 把 acquire/release 的流程写死——尝试获取、失败则入队、阻塞等待、释放后唤醒——但把怎么尝试获取和怎么释放留给子类实现。ReentrantLock 实现的是可重入互斥语义Semaphore 实现的是许可计数语义CountDownLatch 实现的是倒计数语义框架不关心这些差异。这三样东西组合起来就是一个通用的同步器骨架。子类只需要回答两个问题state 代表什么怎么获取和释放它。state同步状态的核心变量AQS 内部维护了一个 volatile int 类型的 state 变量这是整个同步机制的核心。volatile 保证了可见性CAS 操作保证了原子性。// AQS 源码中的核心字段简化privatevolatileintstate;protectedfinalintgetState(){returnstate;}protectedfinalvoidsetState(intnewState){statenewState;}protectedfinalbooleancompareAndSetState(intexpect,intupdate){returnunsafe.compareAndSwapInt(this,stateOffset,expect,update);}state 的含义完全由子类定义同步工具state 的含义state 0 表示ReentrantLock当前线程持有锁的重入计数没有任何线程持有锁Semaphore剩余许可数量没有可用许可CountDownLatch剩计数数量倒计数结束门闩打开ReentrantReadWriteLock高 16 位读锁计数 低 16 位写锁重入数无读锁也无写锁Semaphore 和 CountDownLatch 对 state 的操作方向是相反的。Semaphore 初始化时设置一定的许可数线程获取许可时 state–释放许可时 statestate 归零意味着资源耗尽。CountDownLatch 初始化时设置倒计数每次 countDown() 让 state–state 归零意味着门闩打开所有等待的线程同时放行。AQS 不规定 state 的具体含义把解释权交给同步器实现。它只负责用 CAS 保证 state 变更的原子性用队列管理竞争失败的线程。CLH 队列线程排队的底层结构AQS 使用的是 CLH 队列思想的变体。传统 CLH 锁Craig, Landin, and Hagersten 发明让每个线程不断轮询前驱节点的状态属于自旋等待CPU 一直转。AQS 借鉴了这个通过前驱节点状态协调线程的思想但做了两个关键改造把单向链表改成双向链表方便取消节点时快速修改指针用 LockSupport 的 park/unpark 替代自旋线程阻塞时不消耗 CPU更适合 Java 的线程调度模型。队列中的每个节点是一个 Node 对象// Node 核心字段简化staticfinalclassNode{volatileThreadthread;// 持有的线程volatileNodeprev;// 前驱节点volatileNodenext;// 后继节点volatileintwaitStatus;// 等待状态staticfinalNodeSHAREDnewNode();// 共享模式标记staticfinalNodeEXCLUSIVEnull;// 独占模式标记staticfinalintSIGNAL-1;// 后继节点正在等待当前节点释放时需负责唤醒staticfinalintCANCELLED1;// 节点已取消staticfinalintCONDITION-2;// 在 Condition 队列中等待staticfinalintPROPAGATE-3;// 共享模式下释放后继续传播唤醒}waitStatus 是节点的状态标记0刚入队的默认状态SIGNAL-1表示当前节点的后继节点正在等待资源当前节点释放时需要负责唤醒后继节点。注意方向SIGNAL 标记在前驱节点上含义是我要负责唤醒后面那位CANCELLED1线程放弃了等待超时或被中断节点被标记为取消出队时会被跳过CONDITION-2节点在 Condition 的等待队列中还没转移到 AQS 的同步队列两个关键的 CAS 入队操作// CAS 设置尾节点简化逻辑privateNodeenq(finalNodenode){for(;;){// 自旋直到成功Nodettail;if(tnull){// 队列为空初始化创建一个空的头节点if(compareAndSetHead(newNode()))tailhead;}else{node.prevt;if(compareAndSetTail(t,node)){t.nextnode;returnt;// 入队成功}}}}入队用自旋 CAS 保证在并发环境下只有一个线程能成功把节点接到队尾。队列初始化时会创建一个空的占位头节点dummy node这个头节点不代表任何等待线程只是作为一个锚点。独占模式的完整获取与释放ReentrantLock 使用的是独占模式——同一时刻只有一个线程能持有锁。来看一个线程调用 lock() 之后 AQS 内部发生了什么。获取流程入口是 acquire()// AQS 源码简化publicfinalvoidacquire(intarg){if(!tryAcquire(arg)// 第一步尝试获取acquireQueued(addWaiter(Node.EXCLUSIVE),arg))// 第二步入队等待selfInterrupt();}三步走先尝试获取失败了就入队入队后在队列里自旋等待。addWaiter 把当前线程包装成独占节点CAS 接到队尾。如果 CAS 失败进入 enq 自旋重试直到入队成功。acquireQueued 是等待的核心逻辑关键设计只有前驱节点是 head 时才尝试获取锁。这避免了惊群效应——队列中的线程不会全部被唤醒去抢锁只有最靠近 head 的那个才有资格尝试。如果尝试获取失败它会继续 park等待下一次唤醒。park 的实现很简单privatefinalbooleanparkAndCheckInterrupt(){LockSupport.park(this);// 阻塞当前线程returnThread.interrupted();// 被唤醒后检查中断状态}LockSupport.park() 让线程进入等待状态unpark() 负责唤醒。底层机制是每个线程有一个 permit许可证permit 最大值为 1。unpark 给线程发一个 permit设为 1park 消费一个 permit设为 0。如果 park 时 permit 已经是 1直接消费并返回不会阻塞。这就是为什么 unpark 可以在 park 之前调用——permit 已经准备好了park 发现后立即返回。这个机制比 wait/notify 灵活wait/notify 必须先 wait 再 notify顺序反了通知就丢了。释放流程// AQS 源码简化publicfinalbooleanrelease(intarg){if(tryRelease(arg)){Nodehhead;if(h!nullh.waitStatus!0)unparkSuccessor(h);returntrue;}returnfalse;}privatevoidunparkSuccessor(Nodenode){intwsnode.waitStatus;if(ws0)node.waitStatus0;// 清除 SIGNAL 状态Nodesnode.next;if(snull||s.waitStatus0){// 后继节点已取消从尾部往前找最近的有效节点sfindNodeFromTail(...);}if(s!null)LockSupport.unpark(s.thread);// 唤醒后继节点的线程}release 的逻辑tryRelease 返回 true 表示同步状态已经释放AQS 需要唤醒队列中等待的线程。找到 head 的后继节点unpark 唤醒它。如果后继节点已经取消了就从队尾往前找一个有效的节点。对 ReentrantLock 来说tryRelease 返回 true 意味着 state 减到了 0对 Semaphore 来说释放许可后 state 大于 0 也可能触发唤醒。ReentrantLock 只需要实现 tryAcquire 和 tryRelease// ReentrantLock.NonfairSync.tryAcquire简化protectedbooleantryAcquire(intacquires){intcgetState();if(c0){if(compareAndSetState(0,acquires)){// CAS 抢锁setExclusiveOwnerThread(currentThread());returntrue;}}elseif(currentThread()getExclusiveOwnerThread()){setState(cacquires);// 可重入statereturntrue;}returnfalse;}// tryRelease简化protectedbooleantryRelease(intreleases){intcgetState()-releases;if(currentThread()!getExclusiveOwnerThread())thrownewIllegalMonitorStateException();booleanfree(c0);if(free)setExclusiveOwnerThread(null);setState(c);// 不需要 CAS持锁线程独占修改returnfree;}tryAcquire 里 CAS 只在 state 0 时执行因为这是无锁状态多线程可能同时竞争。重入时直接 setState因为只有持有锁的线程才能走到这个分支不存在竞争。tryRelease 同理——持锁线程独占修改 state不需要 CAS。共享模式Semaphore 和 CountDownLatch 怎么用 AQS独占模式下同一时刻只有一个线程能持有锁。共享模式允许多个线程同时持有资源——Semaphore 的许可可以被多个线程同时获取CountDownLatch 的倒计数可以被多个线程同时递减。共享模式的入口是 acquireSharedpublicfinalvoidacquireShared(intarg){if(tryAcquireShared(arg)0)// 尝试获取共享资源doAcquireShared(arg);// 失败则入队等待}tryAcquireShared 返回值的含义返回正数获取成功还有剩余资源返回 0获取成功没有剩余资源了返回负数获取失败Semaphore 的 tryAcquireShared// Semaphore.NonfairSync.tryAcquireShared简化protectedinttryAcquireShared(intacquires){for(;;){intavailablegetState();intremainingavailable-acquires;if(remaining0||compareAndSetState(available,remaining))returnremaining;}}自旋 CAS 减少许可数。如果 remaining 0说明许可不够直接返回负数否则 CAS 尝试减少失败就重试。共享模式有一个独占模式没有的特性传播propagation。当一个共享节点释放资源后如果 state 仍然大于 0说明还有剩余资源AQS 会继续唤醒队列中的下一个共享节点。这个过程会一直传播下去直到没有剩余资源或者队列中没有共享节点了。这个机制保证了共享锁的高效释放一个读线程释放读锁时如果还有剩余许可会连续唤醒后续的读线程而不是每次只唤醒一个。ReentrantReadWriteLock 用 state 的高 16 位存读锁计数、低 16 位存写锁重入数共享模式管理读锁独占模式管理写锁。读锁可以并发持有写锁必须独占。当写锁释放时如果读等待队列中有共享节点传播机制会把它们全部唤醒。ConditionAQS 里的第二条队列AQS 的同步队列管理的是等待获取锁的线程。但有时候线程拿到锁之后还需要等待某个条件成立才能继续执行——生产者等到队列有空间才能写入消费者等到队列有数据才能取出。Condition 就是用来解决这个问题的。它本质上是基于 AQS 实现的一套条件等待机制——ConditionObject 是 AQS 的内部类直接操作 AQS 的 Node 节点。每个 ConditionObject 内部维护一个独立的单向等待队列和 AQS 的同步队列是两条不同的队列。publicclassConditionObjectimplementsCondition{privateNodefirstWaiter;// 等待队列头privateNodelastWaiter;// 等待队列尾}await() 流程await() 调用 │ ▼ 当前线程包装成 Node加入 Condition 等待队列尾部 │ ▼ 释放锁fullyReleasestate 直接减到 0 │ ▼ LockSupport.park() 阻塞等待被 signal │ ▼ 被唤醒后节点从 Condition 队列转移到 AQS 同步队列 │ ▼ acquireQueued 重新竞争锁signal() 流程signal() 调用 │ ▼ 取出 Condition 队列的头节点 │ ▼ 将节点转移到 AQS 同步队列尾部 │ ▼ 如果前驱节点是 SIGNAL 或已取消唤醒该节点的线程Condition 的优势在于可以创建多个独立的等待队列ReentrantLocklocknewReentrantLock();ConditionnotFulllock.newCondition();// 等待不满的条件ConditionnotEmptylock.newCondition();// 等待不空的条件// 生产者lock.lock();try{while(queue.isFull()){notFull.await();// 队列满了等待消费者腾出空间}queue.add(item);notEmpty.signal();// 通知消费者有数据了}finally{lock.unlock();}// 消费者lock.lock();try{while(queue.isEmpty()){notEmpty.await();// 队列空了等待生产者放入数据}Itemitemqueue.remove();notFull.signal();// 通知生产者有空间了}finally{lock.unlock();}这比 Object.wait()/notify() 灵活。wait/notify 只有一个等待队列notifyAll() 会唤醒所有等待线程包括不该被唤醒的。用两个 Condition生产者只唤醒消费者消费者只唤醒生产者精确控制。小结AQS 的设计可以用四样东西概括一个 volatile int state 表达同步状态一个 FIFO 双向链表队列管理竞争失败的线程CAS 保证状态变更的原子性LockSupport 实现线程的阻塞和唤醒。子类定义 state 的语义和获取/释放的逻辑AQS 负责排队、阻塞、唤醒这些通用机制。ReentrantLock 把 state 当重入计数Semaphore 当许可数CountDownLatch 当倒计数ReadWriteLock 用一个 int 同时塞进读锁和写锁的状态。不同同步器复用同一套状态管理和线程排队机制。理解 AQS 不是为了背源码面试而是理解 JUC 包的设计思路把并发控制的通用逻辑抽出来做成框架让每个同步工具只关注自己的语义。当你遇到需要自定义同步器的场景时AQS 就是你的起点。