Java并发编程之队列同步器AQS(AbstractQueuedSynchronizer)
1. 是什么
你想了解的AQS(AbstractQueuedSynchronizer)是Java并发包(java.util.concurrent)的核心基础组件,是由Doug Lea设计的一个抽象队列同步器,本质是一个用于构建锁(Lock)和同步器(Semaphore、CountDownLatch等)的框架。
其核心内涵:
- 以原子方式管理同步状态(通过
volatile int state变量); - 内置FIFO双向等待队列(CLH队列变体)管理等待获取资源的线程;
- 采用模板方法模式,将同步器的通用逻辑(如队列管理、线程阻塞/唤醒)封装,仅暴露核心逻辑(如获取/释放资源)由子类实现。
关键特征:
- 独占式/共享式:支持独占(如ReentrantLock)和共享(如CountDownLatch)两种资源获取模式;
- 可重入:支持线程重入获取资源(通过记录当前持有线程和重入次数);
- 阻塞与唤醒:基于LockSupport的park/unpark实现线程的安全阻塞和唤醒;
- 非阻塞CAS:核心操作(如状态修改、队列节点入队)依赖CAS保证原子性。
2. 为什么需要
在AQS出现前,你若要实现并发同步,只能依赖synchronized关键字,但它存在明显痛点,而AQS恰好解决了这些问题:
核心痛点
synchronized是JVM层面的隐式锁,灵活性差:无法手动控制锁的获取/释放时机、不支持超时获取、不可中断;- 自定义同步器成本高:若要实现如“计数信号量”“闭锁”等同步工具,需从零实现线程排队、阻塞、唤醒等逻辑,易出错;
- 资源管理混乱:缺乏统一的资源(锁)状态管理机制,不同同步工具的实现逻辑无法复用。
实际应用价值
- 统一基础框架:JUC包中几乎所有同步工具(ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier等)都基于AQS实现,避免重复造轮子;
- 高度可定制:你只需重写AQS的几个核心方法(如
tryAcquire、tryRelease),即可快速实现自定义锁/同步器; - 高性能:基于CAS+自旋+阻塞的混合策略,兼顾非阻塞的高效性和阻塞的资源节约性,性能优于早期
synchronized(JDK1.6后synchronized优化后差距缩小,但灵活性仍不如AQS)。
3. 核心工作模式
AQS的核心运作逻辑围绕“状态管理+队列等待”展开,关键要素及关联如下:
关键要素
| 要素 | 作用 |
|---|---|
| 同步状态(state) | volatile int类型,原子存储资源的可用数量/锁的持有状态(如0=未锁定,1=已锁定) |
| CLH等待队列 | 双向FIFO队列,存储因获取资源失败而阻塞的线程,每个节点对应一个等待线程 |
| 独占/共享模式 | 独占:同一时间仅一个线程获取资源(如ReentrantLock);共享:多个线程可同时获取(如Semaphore) |
| 条件队列(Condition) | 可选的单向队列,用于线程等待/唤醒(如ReentrantLock的Condition) |
| CAS操作 | 保证state修改、队列节点操作的原子性,避免加锁 |
核心机制
- 状态原子操作:通过
getState()/setState()/compareAndSetState()操作state,其中compareAndSetState是CAS核心,保证多线程下状态修改的原子性; - 队列管理机制:
- 线程获取资源失败时,封装为Node节点加入队列尾部(CAS入队);
- 队列头节点是“已获取资源”的线程,后续节点是等待线程;
- 头节点释放资源时,唤醒后继节点尝试获取资源;
- 模板方法+钩子方法:
- 模板方法:AQS提供
acquire(独占获取)、release(独占释放)、acquireShared(共享获取)等通用逻辑; - 钩子方法:子类需重写
tryAcquire(独占获取尝试)、tryRelease(独占释放尝试)、tryAcquireShared(共享获取尝试)等方法,实现具体的资源获取/释放逻辑。
- 模板方法:AQS提供
4. 工作流程
以独占式获取/释放资源(如ReentrantLock)为例,结合流程图拆解完整链路:
核心流程图(Mermaid)
graph TD
A["线程调用acquire()获取资源"] --> B{"调用tryAcquire()尝试获取"}
B -- 成功 --> C["获取资源,流程结束"]
B -- 失败 --> D["封装为Node节点,CAS加入队列尾部"]
D --> E["自旋检查前驱节点是否为头节点"]
E -- 是 --> F["再次尝试tryAcquire()获取资源"]
F -- 成功 --> G["成为新头节点,流程结束"]
F -- 失败 --> H["通过LockSupport.park()阻塞当前线程"]
E -- 否 --> H
I["其他线程调用release()释放资源"] --> J{"调用tryRelease()尝试释放"}
J -- 失败 --> K["释放失败,流程结束"]
J -- 成功 --> L["唤醒队列后继节点线程"]
L --> M["被唤醒的线程回到步骤E,重新自旋尝试获取资源"]
M --> E
分步拆解
(1)独占式获取资源(acquire())
- 线程调用
acquire(int arg)(如ReentrantLock的lock()底层调用); - AQS先调用子类实现的
tryAcquire(arg):- 成功:直接获取资源,流程结束;
- 失败:进入队列等待逻辑;
- 将当前线程封装为
Node.EXCLUSIVE(独占)节点,通过CAS原子性加入队列尾部; - 线程进入自旋(循环):
- 检查前驱节点是否为头节点(说明自己是下一个该获取资源的线程);
- 若是,再次调用
tryAcquire(arg)尝试获取资源; - 若成功:将当前节点设为头节点,流程结束;
- 若失败:通过
LockSupport.park()阻塞当前线程,等待被唤醒;
- 线程被唤醒后,重复自旋逻辑,直到获取资源或中断。
(2)独占式释放资源(release())
- 线程调用
release(int arg)(如ReentrantLock的unlock()底层调用); - AQS调用子类实现的
tryRelease(arg):- 失败:返回false,释放流程结束;
- 成功:继续下一步;
- 获取队列头节点,通过
LockSupport.unpark()唤醒头节点的后继节点线程; - 被唤醒的线程回到自旋逻辑,重新尝试获取资源。
(3)共享式流程差异
共享式(如Semaphore)的核心差异在tryAcquireShared(arg):
- 返回值≥0:表示获取资源成功;
- 返回值<0:表示获取失败,加入队列等待;
- 释放资源时,唤醒的后继节点会继续唤醒其后续节点(因为共享模式允许多个线程同时获取资源)。
5. 入门实操
下面带你实现一个简易的独占式不可重入锁,基于AQS入门实操,让你快速掌握核心用法:
前置条件
- JDK8及以上;
- 熟悉基本的Java多线程知识(线程创建、启动);
- 了解CAS和volatile的基本概念。
实操步骤
步骤1:自定义同步器(继承AQS)
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
// 自定义独占式同步器,实现不可重入锁的核心逻辑
class SimpleExclusiveSync extends AbstractQueuedSynchronizer {
// 1. 尝试获取资源(独占式)
@Override
protected boolean tryAcquire(int arg) {
// CAS修改state:0→1表示获取锁成功
if (compareAndSetState(0, 1)) {
// 设置当前线程为独占锁持有者
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// 不可重入:即使是当前持有线程,再次获取也返回false
return false;
}
// 2. 尝试释放资源(独占式)
@Override
protected boolean tryRelease(int arg) {
// 检查释放线程是否为持有线程
if (Thread.currentThread() != getExclusiveOwnerThread()) {
throw new IllegalMonitorStateException("非锁持有者无法释放锁");
}
// 释放锁:state重置为0
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 3. 判断是否处于独占模式(AQS模板方法需要)
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 对外暴露锁的获取/释放方法
public void lock() {
acquire(1); // 调用AQS的模板方法acquire
}
public void unlock() {
release(1); // 调用AQS的模板方法release
}
}
步骤2:封装为锁工具类
// 自定义不可重入锁
public class SimpleExclusiveLock {
// 内部持有自定义同步器
private final SimpleExclusiveSync sync = new SimpleExclusiveSync();
// 获取锁
public void lock() {
sync.lock();
}
// 释放锁
public void unlock() {
sync.unlock();
}
}
步骤3:测试锁的功能
public class AQSDemo {
private static int count = 0;
private static final SimpleExclusiveLock lock = new SimpleExclusiveLock();
// 累加任务
private static void increment() {
lock.lock(); // 获取锁
try {
count++;
System.out.println(Thread.currentThread().getName() + ":count = " + count);
Thread.sleep(100); // 模拟业务耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock(); // 释放锁(必须在finally中,避免死锁)
}
}
public static void main(String[] args) throws InterruptedException {
// 创建10个线程,每个线程执行1次累加
for (int i = 0; i < 10; i++) {
new Thread(AQSDemo::increment, "线程" + i).start();
}
// 等待所有线程执行完毕
Thread.sleep(2000);
System.out.println("最终count值:" + count); // 预期输出10
}
}
关键操作要点
- 钩子方法重写规则:
- 独占模式:必须重写
tryAcquire和tryRelease,可选重写isHeldExclusively; - 共享模式:必须重写
tryAcquireShared和tryReleaseShared; - 不要重写AQS的模板方法(如
acquire、release),仅重写钩子方法;
- 独占模式:必须重写
- 资源释放必须在finally中:避免线程异常导致锁无法释放,引发死锁;
- CAS操作的参数:
compareAndSetState的预期值和新值需根据业务逻辑设定(如不可重入锁用0→1,可重入锁需累加state)。
实操注意事项
- 不可重入锁的局限性:若线程在持有锁的情况下再次调用
lock(),会导致自己阻塞(死锁),如需重入,需修改tryAcquire逻辑(判断当前线程是否为持有者,若是则累加state); - 未处理中断:上述示例中
acquire()是不可中断的,若需支持中断,可使用acquireInterruptibly(); - 无超时机制:可使用
tryAcquireNanos()实现超时获取锁,避免线程永久阻塞。
6. 常见问题及解决方案
问题1:AQS实现的锁出现死锁
现象
线程阻塞后无法被唤醒,程序卡死,jstack查看线程状态为WAITING (parking)。
原因
- 释放锁时未在
finally中执行unlock(),导致线程异常退出后锁未释放; - 重入锁的
tryRelease逻辑错误(如state未正确递减,导致tryRelease返回false,无法唤醒后继节点); - 自定义同步器的
tryAcquire逻辑错误,导致线程重复入队且无法获取资源。
解决方案
- 强制释放锁在finally中:所有
lock()后必须在finally块中调用unlock(),示例:lock.lock(); try { // 业务逻辑 } finally { lock.unlock(); } - 正确实现可重入锁的释放逻辑:
@Override protected boolean tryRelease(int arg) { if (Thread.currentThread() != getExclusiveOwnerThread()) { throw new IllegalMonitorStateException(); } int newState = getState() - arg; boolean free = (newState == 0); if (free) { setExclusiveOwnerThread(null); } setState(newState); // 先修改state,再释放持有线程 return free; } - 通过jstack排查死锁:执行
jps获取进程ID,再执行jstack <pid>,查看OWNABLE_SYNCHRONIZERS部分,定位持有锁的线程和等待锁的线程。
问题2:共享模式下线程唤醒不彻底
现象
Semaphore等共享同步器中,释放资源后仅部分等待线程被唤醒,其余线程仍阻塞。
原因
共享模式下tryReleaseShared返回true后,AQS仅唤醒后继节点,但后继节点获取资源后未继续唤醒其后续节点(共享模式需“传播”唤醒)。
解决方案
- 正确实现
tryReleaseShared:返回true表示需要唤醒后继节点; - 确保共享释放的传播性:AQS的
releaseShared会自动处理传播唤醒,只需保证tryReleaseShared逻辑正确,示例:@Override protected int tryAcquireShared(int arg) { // 共享获取:state >= arg时成功,否则失败 for (;;) { int current = getState(); int newCount = current - arg; if (newCount < 0 || compareAndSetState(current, newCount)) { return newCount; // <0失败,≥0成功 } } } @Override protected boolean tryReleaseShared(int arg) { // 共享释放:CAS累加state for (;;) { int current = getState(); int newCount = current + arg; if (compareAndSetState(current, newCount)) { return true; // 返回true触发唤醒 } } }
问题3:AQS同步器性能低下
现象
高并发下,基于AQS的同步器(如ReentrantLock)出现大量线程自旋、上下文切换,CPU使用率高但吞吐量低。
原因
- 自旋次数过多:AQS默认的自旋逻辑在高竞争下会导致线程空转,消耗CPU;
- 阻塞/唤醒频繁:大量线程进入等待队列,频繁的park/unpark导致上下文切换;
- 同步器选择不当:独占锁用于高并发读场景(应使用共享锁或读写锁)。
解决方案
- 使用带超时的获取方法:避免线程无限自旋/阻塞,示例:
// 尝试获取锁,500ms超时 if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { try { // 业务逻辑 } finally { lock.unlock(); } } else { // 超时处理,避免自旋 return; } - 选择合适的同步模式:读多写少场景使用ReentrantReadWriteLock(读共享、写独占),减少锁竞争;
- 优化自旋策略:自定义AQS时,可在
acquire前增加有限次数的自旋(如3次),减少入队概率:public void lock() { // 先自旋3次尝试获取锁,失败再走AQS的acquire int spinCount = 0; while (spinCount < 3 && !tryAcquire(1)) { spinCount++; Thread.yield(); // 让步,减少CPU消耗 } if (!tryAcquire(1)) { acquire(1); } }
总结
- 核心定位:AQS是JUC并发工具的基础框架,通过
state管理资源状态、CLH队列管理等待线程,采用模板方法模式支持自定义同步器; - 核心逻辑:线程获取资源时先尝试CAS修改state,失败则入队自旋/阻塞;释放资源时修改state并唤醒队列后继线程,独占/共享模式的核心差异在资源获取逻辑;
- 实操关键:重写AQS的钩子方法(
tryAcquire/tryRelease等),释放锁必须在finally中,根据场景选择独占/共享模式,避免死锁和性能问题。

浙公网安备 33010602011771号