AQS 原理详解
💡 提示: 知识图谱 查看 AQS知识图谱 获取可视化知识结构
什么是 AQS? AQS (AbstractQueuedSynchronizer,抽象队列同步器)是 Java 并发包 java.util.concurrent.locks 中的核心基础框架。
一句话理解
AQS 就像一个排队系统 :想要获取资源的线程,如果资源被占用,就乖乖排队等待;资源释放后,排在最前面的线程被唤醒去获取资源。
为什么需要 AQS? 在 AQS 出现之前,如果我们想实现一个锁或者同步器,需要自己处理:
AQS 把这些通用逻辑 抽取出来,我们只需要关注资源的获取和释放规则 即可。
核心概念 1. State(同步状态) AQS 使用一个 int 类型的变量 state 来表示同步状态:
1 private volatile int state;
同步器类型
state 含义
ReentrantLock
0 表示未锁定,>0 表示锁定次数(可重入)
Semaphore
表示可用的许可数量
CountDownLatch
表示需要等待的计数
2. CLH 队列(等待队列) 当线程获取资源失败时,会被包装成一个 Node 节点 ,加入到一个 FIFO 双向队列 中等待。
1 2 3 4 5 +------+ prev +------+ prev +------+ head | | <---- | | <---- | | tail | Node | | Node | | Node | | | ----> | | ----> | | +------+ next +------+ next +------+
3. Node 节点 每个等待的线程都被封装成一个 Node:
1 2 3 4 5 6 7 8 9 10 11 12 static final class Node { volatile int waitStatus; volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter; }
waitStatus 的取值:
值
常量名
含义
0
-
初始状态
1
CANCELLED
线程已取消
-1
SIGNAL
后继节点需要被唤醒
-2
CONDITION
在条件队列中等待
-3
PROPAGATE
共享模式下传播唤醒
两种资源共享模式 独占模式(Exclusive) 同一时刻只有一个线程能获取资源
典型实现:ReentrantLock
1 2 3 4 线程A 获取锁 → 成功,执行业务 线程B 获取锁 → 失败,进入队列等待 线程C获取锁 → 失败,进入队列等待 线程A 释放锁 → 唤醒线程B
共享模式(Shared) 同一时刻可以有多个线程获取资源
典型实现:Semaphore、CountDownLatch、ReadWriteLock(读锁)
1 2 3 4 5 Semaphore (3 ) - 允许3 个线程同时访问 线程A获取 → 成功,剩余2 个许可 线程B获取 → 成功,剩余1 个许可 线程C获取 → 成功,剩余0 个许可 线程D获取 → 失败,进入队列等待
核心方法解析 需要子类实现的方法 AQS 采用模板方法模式 ,子类需要实现以下方法:
方法
模式
说明
tryAcquire(int)
独占
尝试获取资源
tryRelease(int)
独占
尝试释放资源
tryAcquireShared(int)
共享
尝试获取共享资源
tryReleaseShared(int)
共享
尝试释放共享资源
isHeldExclusively()
-
是否独占持有
AQS 提供的模板方法
方法
说明
acquire(int)
独占式获取(会阻塞)
acquireInterruptibly(int)
独占式获取(可中断)
tryAcquireNanos(int, long)
独占式获取(带超时)
release(int)
独占式释放
acquireShared(int)
共享式获取
releaseShared(int)
共享式释放
独占模式源码分析 acquire() - 获取资源 1 2 3 4 5 public final void acquire (int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
执行流程:
flowchart TD
A[调用 acquire] --> B{tryAcquire 尝试获取}
B -->|成功| C[直接返回,获取到资源]
B -->|失败| D[addWaiter 创建节点加入队列]
D --> E[acquireQueued 在队列中自旋等待]
E --> F{是否被中断过}
F -->|是| G[selfInterrupt 补上中断]
F -->|否| H[返回]
addWaiter() - 加入等待队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private Node addWaiter (Node mode) { Node node = new Node (Thread.currentThread(), mode); Node pred = tail; if (pred != null ) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
acquireQueued() - 队列中等待获取 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 final boolean acquireQueued (final Node node, int arg) { boolean failed = true ; try { boolean interrupted = false ; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null ; failed = false ; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true ; } } finally { if (failed) cancelAcquire(node); } }
关键点:
只有前驱节点是 head 的节点才有资格尝试获取资源
获取失败后会调用 LockSupport.park() 阻塞线程
被唤醒后继续自旋尝试获取
release() - 释放资源 1 2 3 4 5 6 7 8 9 public final boolean release (int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0 ) unparkSuccessor(h); return true ; } return false ; }
图解:完整的获取-释放流程 场景:三个线程竞争一把锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 初始状态:state = 0,队列为空=== 线程A 获取锁 === 1. tryAcquire(1) → CAS(0, 1) 成功 2. state = 1,线程A持有锁 3. 队列仍为空=== 线程B 获取锁 === 1. tryAcquire(1) → 失败(state=1) 2. addWaiter() → 创建Node(B),初始化队列 +------+ +------+ | head | ---> | B | | 空 | | 等待 | +------+ +------+ 3. acquireQueued() → park阻塞=== 线程C 获取锁 === 1. tryAcquire(1) → 失败 2. addWaiter() → 创建Node(C)加入队尾 +------+ +------+ +------+ | head | ---> | B | ---> | C | | 空 | | 等待 | | 等待 | +------+ +------+ +------+ 3. acquireQueued() → park阻塞=== 线程A 释放锁 === 1. tryRelease(1) → state = 0 2. unparkSuccessor(head) → 唤醒线程B 3. 线程B被唤醒,tryAcquire成功 4. 线程B成为新的head +------+ +------+ | B | ---> | C | | head | | 等待 | +------+ +------+
实战:手写一个简单的互斥锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class SimpleLock { private final Sync sync = new Sync (); private static class Sync extends AbstractQueuedSynchronizer { @Override protected boolean tryAcquire (int arg) { if (compareAndSetState(0 , 1 )) { setExclusiveOwnerThread(Thread.currentThread()); return true ; } return false ; } @Override protected boolean tryRelease (int arg) { if (getState() == 0 ) { throw new IllegalMonitorStateException (); } setExclusiveOwnerThread(null ); setState(0 ); return true ; } @Override protected boolean isHeldExclusively () { return getExclusiveOwnerThread() == Thread.currentThread(); } } public void lock () { sync.acquire(1 ); } public void unlock () { sync.release(1 ); } public boolean tryLock () { return sync.tryAcquire(1 ); } }
使用示例:
1 2 3 4 5 6 7 8 9 SimpleLock lock = new SimpleLock (); lock.lock();try { System.out.println("获取到锁,执行业务逻辑" ); } finally { lock.unlock(); }
AQS 的经典实现类
实现类
模式
说明
ReentrantLock
独占
可重入互斥锁
ReentrantReadWriteLock
共享+独占
读写锁
CountDownLatch
共享
倒计时门闩
Semaphore
共享
信号量
CyclicBarrier
-
循环栅栏(内部用ReentrantLock)
核心要点总结 AQS 的设计精髓
模板方法模式 :AQS 定义算法骨架,子类实现具体逻辑
CLH 队列变体 :高效的 FIFO 等待队列
CAS + volatile :保证线程安全的无锁操作
LockSupport :精确的线程阻塞/唤醒控制
记忆口诀 1 2 3 4 AQS三要素:状态、队列、CAS 独占看try Acquire,共享看try AcquireShared 获取失败进队列,前驱是head才能抢 释放资源唤后继,FIFO保公平
常见面试题 Q1:AQS 为什么使用双向链表?
方便取消操作 :取消节点时需要修改前驱的 next 指针
方便判断前驱状态 :只有前驱是 head 才能尝试获取资源
方便从尾部遍历 :某些场景需要从后向前遍历
Q2:为什么 head 节点是虚节点?
head 代表当前持有资源的线程 (或初始空节点)
真正等待的线程从 head.next 开始
这样设计简化了边界条件处理
Q3:公平锁和非公平锁的区别?
类型
实现方式
特点
公平锁
tryAcquire 时先检查队列
严格 FIFO,吞吐量较低
非公平锁
tryAcquire 时直接 CAS 抢锁
可能插队,吞吐量较高
1 2 3 4 5 if (state == 0 && CAS(0 , 1 )) return true ;if (state == 0 && !hasQueuedPredecessors() && CAS(0 , 1 )) return true ;
延伸阅读
ReentrantLock 原理 - AQS 的典型独占实现
CountDownLatch 原理 - AQS 的典型共享实现
CAS 原理 - AQS 的底层原子操作