)
在Java并发编程中我们经常使用ReentrantLock、Semaphore、CountDownLatch等同步工具却很少思考这些工具背后的“统一底层支撑”——它们本质上都是基于AbstractQueuedSynchronizer简称AQS实现的。AQS是Java并发包java.util.concurrent.locks的核心骨架定义了一套通用的同步器框架封装了线程排队、锁获取与释放、中断响应等核心逻辑让开发者无需重复实现复杂的同步机制就能快速构建安全高效的同步工具。本文将从AQS的核心定位出发逐步拆解其底层结构、核心机制、两种工作模式结合源码片段和实战案例详细讲解AQS的工作原理让你不仅能“知其然”更能“知其所以然”彻底搞懂Java并发同步的底层逻辑。一、先搞懂AQS是什么为什么需要它1. AQS的核心定位AQS的全称是AbstractQueuedSynchronizer翻译为“抽象队列同步器”它是一个抽象类本身不直接实现同步功能而是提供了一套同步器的“模板骨架”——定义了线程竞争资源的核心流程排队、阻塞、唤醒将具体的“资源获取/释放逻辑”交给子类去实现。简单来说AQS的核心作用是统一管理线程的排队和调度封装通用同步逻辑简化同步工具的开发。如果没有AQSReentrantLock、Semaphore等工具都需要各自实现线程排队、阻塞唤醒等逻辑会导致代码冗余、效率低下且难以保证线程安全。2. 为什么需要AQS在并发编程中线程之间的竞争与协作是核心问题而同步工具的核心需求是“控制线程对资源的访问权限”。如果没有统一的框架开发者实现同步逻辑时会面临两个核心难题线程竞争无序多个线程同时竞争资源时容易出现“插队”现象导致公平性无法保证甚至出现死锁线程调度低效线程获取资源失败后若一直自旋重试会浪费大量CPU资源若直接终止又无法保证后续能重新获取资源。AQS通过“状态管理队列调度”的组合完美解决了这两个问题用一个volatile状态变量控制资源访问权限用一个FIFO双向队列管理等待线程实现了线程的有序排队和高效调度同时支持公平/非公平两种模式兼顾性能与公平性。3. AQS的核心设计思想AQS的设计遵循“模板方法模式”核心思想可以概括为3点也是AQS的三大核心组成部分一个volatile状态变量state用于表示资源的占用状态子类通过操作该状态实现资源的获取与释放一个FIFO双向等待队列用于存储获取资源失败的线程线程进入队列后会阻塞等待被唤醒后重新竞争资源一套模板方法钩子方法AQS定义了获取/释放资源的核心流程模板方法将具体的资源操作逻辑钩子方法交给子类实现无需子类关心排队和调度细节。一句话总结AQS做“通用的事”排队、阻塞、唤醒子类做“具体的事”资源的获取与释放分工明确灵活高效。二、AQS底层结构拆解状态队列缺一不可AQS的底层核心是“状态变量state”和“双向等待队列”两者协同工作实现线程的同步与调度。下面我们逐一拆解这两个核心组件结合源码片段搞懂其底层实现。1. 核心状态变量volatile int state(1)state的作用state是AQS中最核心的成员变量用volatile修饰保证了线程之间的可见性用于表示资源的占用状态其具体含义由子类决定AQS不固定state的含义。常见的state含义ReentrantLock中state表示“锁的重入次数”0表示锁未被占用大于0表示锁被占用state的值等于重入次数Semaphore中state表示“可用资源的数量”0表示无可用资源线程需排队等待CountDownLatch中state表示“倒计时计数器”0表示倒计时结束所有等待线程被唤醒。(2)state的核心操作方法AQS提供了3个核心方法操作state均基于CAS实现保证操作的原子性避免线程安全问题子类可直接调用getState()获取当前state的值简单的读操作因为state是volatile的无需加锁setState(int newState)设置state的值仅在子类确定当前线程拥有操作权限时使用如释放锁时compareAndSetState(int expect, int update)CAS操作仅当当前state的值等于expect时将其更新为update返回true否则返回false是实现线程安全竞争的核心方法。示例源码AQS中state相关定义java// 核心状态变量volatile保证可见性 private volatile int state; // 获取当前状态 protected final int getState() { return state; } // 设置状态无CAS需子类保证线程安全 protected final void setState(int newState) { state newState; } // CAS更新状态原子操作 protected final boolean compareAndSetState(int expect, int update) { return unsafe.compareAndSwapInt(this, stateOffset, expect, update); }注意AQS本身不限制state的取值范围和含义完全由子类根据自身需求定义这也是AQS灵活性的核心体现。2. 双向等待队列CLH变体队列当线程通过CAS获取state失败即资源被其他线程占用时不会一直自旋重试而是会被封装成一个“节点Node”加入到AQS的双向等待队列中然后阻塞自己等待被前驱节点唤醒——这个队列是AQS实现线程排队的核心本质是一个CLH锁队列的变体CLH是一种基于链表的无锁队列用于实现公平锁。(1) 队列的结构AQS的等待队列是一个FIFO先进先出双向链表每个节点代表一个等待中的线程队列有两个核心指针head头节点和tail尾节点初始时head和tail都为null当有线程加入队列时tail指针不断后移head指针仅在有线程释放资源、唤醒后继线程时移动。队列结构示意图简化texthead哨兵节点 ←→ Node1线程1 ←→ Node2线程2 ←→ ... ←→ tail尾节点说明head节点是一个“哨兵节点”空节点不对应任何等待线程其作用是简化队列的操作避免判断head是否为null真正的等待线程从head的后继节点开始。(2)Node节点的核心属性每个Node节点封装了等待线程的相关信息核心属性如下源码简化版javastatic final class Node { // 标记节点的等待状态核心属性 volatile int waitStatus; // 前驱节点 volatile Node prev; // 后继节点 volatile Node next; // 当前节点对应的线程 volatile Thread thread; // 条件队列相关后续讲解 Node nextWaiter; // 等待状态的常量值 static final int CANCELLED 1; // 线程被取消如超时、中断 static final int SIGNAL -1; // 后继节点需要被唤醒 static final int CONDITION -2; // 线程在条件队列中等待 static final int PROPAGATE -3; // 共享模式下状态需要传播 }其中waitStatus是Node节点的核心属性用于标记当前线程的等待状态决定了线程的行为是否需要阻塞、是否能被唤醒各状态的含义如下CANCELLED1线程因超时或被中断已取消等待该节点会被从队列中移除不再参与资源竞争SIGNAL-1当前节点的后继节点正在阻塞当前节点释放资源后需要唤醒后继节点CONDITION-2线程在条件队列中等待通过Condition.await()方法进入需等待条件满足后被唤醒PROPAGATE-3仅用于共享模式标识当前资源的释放状态需要向后传播唤醒所有等待的线程0默认状态线程正常等待未被取消、未被标记为唤醒后继节点。(3) 队列的核心操作入队与出队AQS封装了队列的入队enq和出队deq操作均基于CAS实现保证线程安全子类无需关心具体实现核心流程如下入队操作enq线程获取资源失败后调用enq方法将节点加入队列尾部。若队列为空head和tail都为null先创建一个哨兵节点作为head再将当前节点作为tail若队列不为空通过CAS将当前节点设置为新的tail同时将原tail的next指向当前节点入队完成后线程调用LockSupport.park()方法阻塞自己等待被唤醒。出队操作deq线程释放资源后会唤醒head的后继节点该节点获取资源成功后将自己设置为新的head原head节点被回收完成出队。出队仅发生在“线程获取资源成功”时由获取资源成功的线程主动完成出队后新的head节点仍为哨兵节点保证队列结构的一致性。核心总结队列的入队和出队操作均为线程安全通过CAS避免并发问题FIFO的结构保证了线程排队的有序性哨兵节点的设计简化了队列操作。三、AQS核心机制模板方法两种工作模式AQS的核心竞争力在于“模板方法模式”的设计以及支持“独占模式”和“共享模式”两种工作模式适配不同的同步场景如独占锁、共享锁。下面我们详细讲解这两个核心机制结合源码理解AQS的工作流程。1. 模板方法模式AQS的“骨架”AQS定义了一套“获取资源”和“释放资源”的模板方法这些方法是final修饰的子类无法重写保证了核心流程的一致性同时提供了一组“钩子方法”protected修饰空实现或抛出异常子类需要根据自身需求重写这些钩子方法实现具体的资源获取/释放逻辑。(1) 核心模板方法获取资源AQS提供了两种获取资源的模板方法对应两种工作模式核心逻辑一致先尝试获取资源获取失败则入队阻塞等待被唤醒后重新尝试。独占模式获取资源acquire(int arg)对应ReentrantLock等独占锁同一时刻只有一个线程能获取资源。java// 独占模式获取资源模板方法final不可重写 public final void acquire(int arg) { // 1. 尝试获取资源钩子方法子类实现 // 2. 若获取失败将当前线程封装为Node加入队列 // 3. 阻塞当前线程等待被唤醒后重新尝试获取 if (!tryAcquire(arg) acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { // 若线程被中断补充中断响应 selfInterrupt(); } }共享模式获取资源acquireShared(int arg)对应Semaphore、CountDownLatch等共享锁同一时刻多个线程可获取资源。java// 共享模式获取资源模板方法final不可重写 public final void acquireShared(int arg) { // 1. 尝试获取共享资源钩子方法子类实现 // 2. 若获取失败入队阻塞 if (tryAcquireShared(arg) 0) { doAcquireShared(arg); } }(2) 核心模板方法释放资源释放资源的模板方法同样分为独占模式和共享模式核心逻辑是释放资源更新state然后唤醒队列中的后继线程让其重新竞争资源。独占模式释放资源release(int arg)javapublic final boolean release(int arg) { // 1. 尝试释放资源钩子方法子类实现 if (tryRelease(arg)) { // 2. 唤醒head的后继节点 Node h head; if (h ! null h.waitStatus ! 0) { unparkSuccessor(h); } return true; } return false; }共享模式释放资源releaseShared(int arg)javapublic final boolean releaseShared(int arg) { // 1. 尝试释放共享资源钩子方法子类实现 if (tryReleaseShared(arg)) { // 2. 唤醒后继节点共享模式可能需要唤醒多个线程 doReleaseShared(); return true; } return false; }(3) 钩子方法子类需重写AQS的钩子方法均为protected修饰子类根据自身需求重写未重写则抛出UnsupportedOperationException异常核心钩子方法如下钩子方法模式作用示例子类tryAcquire(int arg)独占模式尝试获取独占资源返回true表示获取成功false表示失败ReentrantLocktryRelease(int arg)独占模式尝试释放独占资源返回true表示释放成功false表示失败ReentrantLocktryAcquireShared(int arg)共享模式尝试获取共享资源返回0表示获取成功0表示失败Semaphore、CountDownLatchtryReleaseShared(int arg)共享模式尝试释放共享资源返回true表示释放成功false表示失败Semaphore、CountDownLatchisHeldExclusively()独占模式判断当前线程是否独占资源用于Condition相关操作ReentrantLock核心总结模板方法定义了“获取-排队-阻塞-唤醒-释放”的完整流程钩子方法定义了“资源如何获取/释放”子类只需重写钩子方法就能快速实现同步工具——这就是AQS的强大之处。2. 两种工作模式独占模式 vs 共享模式AQS支持两种资源竞争模式分别对应不同的同步场景子类可根据需求选择实现其中一种模式也可同时实现两种模式如ReentrantReadWriteLock读锁是共享模式写锁是独占模式。(1) 独占模式Exclusive Mode核心特点同一时刻只有一个线程能获取资源其他线程只能排队等待直到持有资源的线程释放资源。适用场景独占锁如ReentrantLock、互斥同步确保同一时刻只有一个线程执行临界区代码核心逻辑线程获取资源时需判断state是否为0未被占用若为0则通过CAS将state设为1或重入次数获取成功若不为0则入队阻塞释放资源时将state重置为0唤醒后继线程。补充独占模式支持“重入”如ReentrantLock即持有资源的线程可再次获取资源只需将state自增重入次数1释放时自减直到state为0时真正释放资源。(2) 共享模式Shared Mode核心特点同一时刻多个线程可同时获取资源资源的可用数量会随着线程的获取而减少随着线程的释放而增加。适用场景共享锁如Semaphore的许可、CountDownLatch的倒计时、ReentrantReadWriteLock的读锁核心逻辑线程获取资源时判断state是否大于0有可用资源若大于0则通过CAS减少state可用资源-1获取成功若等于0则入队阻塞释放资源时通过CAS增加state可用资源1唤醒所有等待的线程因为多个线程可同时获取资源。补充共享模式下线程获取资源后不会阻止其他线程获取资源只要有可用资源适合“读多写少”的场景如缓存读取。(3) 两种模式对比对比维度独占模式共享模式资源占用同一时刻仅一个线程占用同一时刻多个线程占用state含义锁的重入次数0表示未占用可用资源数量0表示无可用唤醒逻辑释放资源时仅唤醒一个后继线程释放资源时唤醒所有等待线程代表子类ReentrantLock写锁Semaphore、CountDownLatch、ReentrantReadWriteLock读锁四、实战解析AQS在ReentrantLock中的应用光懂理论不够我们结合ReentrantLock可重入独占锁的源码看看AQS的钩子方法是如何被重写的以及AQS的核心流程如何工作——ReentrantLock是AQS独占模式的典型实现也是我们日常开发中最常用的同步工具之一。1.ReentrantLock的核心实现ReentrantLock内部有一个静态内部类Sync该类继承自AQS重写了AQS的tryAcquire、tryRelease、isHeldExclusively三个钩子方法实现了独占模式的资源获取与释放。ReentrantLock支持公平锁和非公平锁两者的区别仅在于tryAcquire方法的实现不同。(1) 非公平锁的tryAcquire实现默认非公平锁的特点线程获取资源时不会先判断队列是否有等待线程而是直接尝试CAS获取资源若获取成功则直接持有锁效率更高但可能导致线程饥饿。javastatic final class NonfairSync extends Sync { private static final long serialVersionUID 7316153563782823691L; // 重写AQS的tryAcquire方法尝试获取独占资源 protected final boolean tryAcquire(int acquires) { final Thread current Thread.currentThread(); int c getState(); // 1. 若state为0锁未被占用直接CAS获取锁 if (c 0) { if (compareAndSetState(0, acquires)) { // 标记当前线程为锁的持有者 setExclusiveOwnerThread(current); return true; } } // 2. 若当前线程已持有锁重入state自增 else if (current getExclusiveOwnerThread()) { int nextc c acquires; if (nextc 0) // 溢出判断 throw new Error(Maximum lock count exceeded); setState(nextc); return true; } // 3. 锁被其他线程持有获取失败 return false; } }(2)tryRelease方法的实现释放锁时减少state重入次数当state减为0时标记锁的持有者为null真正释放锁。javaprotected final boolean tryRelease(int releases) { int c getState() - releases; // 只有锁的持有者才能释放锁 if (Thread.currentThread() ! getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free false; // 当state减为0时释放锁标记持有者为null if (c 0) { free true; setExclusiveOwnerThread(null); } setState(c); return free; }2.ReentrantLock的工作流程结合AQS以非公平锁为例ReentrantLock的lock()和unlock()方法的工作流程本质就是AQS模板方法的执行流程线程调用lock()方法底层调用AQS的acquire(1)模板方法acquire(1)调用ReentrantLock重写的tryAcquire(1)方法尝试获取资源若state为0CAS将state设为1标记当前线程为持有者获取成功流程结束若当前线程已持有锁重入state自增1获取成功流程结束若锁被其他线程持有tryAcquire返回false进入下一步。AQS将当前线程封装为Node节点加入双向等待队列调用LockSupport.park()阻塞线程持有锁的线程调用unlock()方法底层调用AQS的release(1)模板方法release(1)调用ReentrantLock重写的tryRelease(1)方法state减1若state减为0释放锁标记持有者为null返回trueAQS唤醒head的后继节点被唤醒的线程重新尝试获取资源重复步骤2。五、AQS进阶条件变量Condition除了核心的状态管理和队列调度AQS还提供了条件变量Condition的支持用于实现“线程等待-唤醒”的精细化控制——Condition类似于Object的wait()和notify()方法但比其更灵活支持多个条件队列。1.Condition的核心作用Condition依托于AQS实现每个Condition对应一个条件队列单向链表用于存储调用Condition.await()方法后阻塞的线程。当条件满足时调用Condition.signal()或signalAll()方法唤醒条件队列中的线程让其重新竞争资源。核心优势一个锁可以对应多个Condition实现不同条件下的线程等待与唤醒例如生产者-消费者模型中可通过两个Condition分别管理“生产者等待队列”和“消费者等待队列”避免不必要的唤醒。2.Condition的核心方法await()线程调用该方法后释放所持有的锁进入当前Condition的条件队列阻塞自己等待被唤醒signal()唤醒条件队列中的第一个线程将其转移到AQS的等待队列中让其重新竞争锁signalAll()唤醒条件队列中的所有线程将其全部转移到AQS的等待队列中。3.Condition与AQS队列的关系AQS有两个核心队列同步队列双向链表用于存储获取锁失败的线程和条件队列单向链表用于存储Condition.await()阻塞的线程两者的关系如下线程调用Condition.await()时会先释放锁然后从同步队列中移除加入到条件队列中阻塞自己线程调用Condition.signal()时会将条件队列中的线程移出加入到同步队列中等待重新获取锁条件队列是依赖于同步队列存在的只有持有锁的线程才能调用Condition的await()和signal()方法否则会抛出IllegalMonitorStateException异常。六、常见误区与注意事项学习AQS时很容易陷入一些误区这里总结几个高频误区帮助大家避坑误区1AQS是一个锁——错误。AQS不是锁而是一个同步器框架是实现锁和同步工具的“底层骨架”ReentrantLock、Semaphore等才是锁/同步工具。误区2state的值只能是0或1——错误。state的含义由子类定义可根据需求取任意整数如ReentrantLock中state是重入次数可大于1Semaphore中state是可用资源数可大于1。误区3AQS的队列是单向链表——错误。AQS的等待队列是双向链表双向链表的优势是便于节点的删除如线程被取消时可快速调整前驱和后继节点的指针。误区4共享模式下所有线程都能获取资源——错误。共享模式下线程能否获取资源取决于state的值可用资源数当state为0时线程仍需排队等待。注意事项子类重写钩子方法时必须保证线程安全通常基于CAS操作Condition的await()和signal()方法必须在持有锁的情况下调用否则会抛出异常。七、总结AQS的核心价值与应用场景AQS作为Java并发包的核心骨架其核心价值在于“封装通用同步逻辑简化同步工具的开发”——它将线程排队、阻塞唤醒、状态管理等复杂逻辑封装起来让开发者只需关注“资源的获取与释放”就能快速实现安全高效的同步工具。AQS的核心本质可以概括为一个volatile状态变量state 一个FIFO双向等待队列 一套模板方法 两种工作模式四种组件协同工作实现了线程的有序竞争与高效调度。常见的基于AQS实现的同步工具独占模式ReentrantLock可重入独占锁、ReentrantLock.WriteLock写锁共享模式Semaphore信号量、CountDownLatch倒计时器、CyclicBarrier循环屏障、ReentrantLock.ReadLock读锁。掌握AQS不仅能让你更好地理解Java并发工具的底层原理在遇到并发问题时还能基于AQS自定义同步工具满足特定业务场景的需求——这也是Java并发编程从“会用”到“精通”的关键一步。