AbstractQueuedSynchronized为什么采用双向链表
AbstractQueuedSynchronizer(简称 AQS)是 Java 并发包(java.util.concurrent.locks)中最核心的同步框架之一,如 ReentrantLock、Semaphore、CountDownLatch、FutureTask 等都基于它实现。
在 AQS 的实现中,它使用了一个双向链表(双向等待队列)来管理获取同步状态失败的线程。
一、AQS 为什么采用双向链表?
AQS 使用的队列实际上是一个 CLH(Craig, Landin, and Hagersten)变种队列,是一个FIFO 的双向链表结构,用于线程的排队与唤醒控制。
核心理由:支持线程阻塞排队 + 高效唤醒 + 状态传播。
二、为什么不用单向链表?
AQS 选择双向链表的关键原因如下:
| 原因 | 说明 |
|---|---|
| ✅ 快速找到前驱节点 | 用于判断当前线程是否是队列头的下一个(是否可唤醒) |
| ✅ 节点取消后可快速清理链表 | 若某线程超时或中断退出,可快速通过 prev 指针修复队列 |
| ✅ 高效传播唤醒信号 | 需要向前/向后传播唤醒状态 |
| ✅ 灵活支持公平锁/非公平锁 | 前驱可控,有利于判断是否排队、可重入 |
| ❌ 单向链表无法向前遍历 | 难以实现超时/中断后清理节点和队列修复 |
| ❌ 单向链表无法清晰表达“阻塞关系” | AQS 需要维护谁阻塞谁、谁唤醒谁的明确关系 |
三、AQS 中双向链表结构简述
AQS 的双向链表维护的不是线程,而是Node 节点:
static final class Node {
volatile Node prev; // 前驱
volatile Node next; // 后继
volatile Thread thread;
volatile int waitStatus; // 状态值(SIGNAL, CANCELLED 等)
}
链表结构示意:
head <-> node1 <-> node2 <-> node3 ... <-> tail
特点:
- 每个线程获取锁失败后,会被封装成一个 Node 放入链表末尾
- 前驱节点决定当前节点是否可以继续尝试获取锁
- 如果当前节点被取消了(如中断、超时),可通过
prev/next快速修复结构
四、真实场景下的操作演示
1. 入队
tail.next = newNode;
newNode.prev = tail;
tail = newNode;
2. 出队唤醒
if (head.next.waitStatus == Node.SIGNAL) {
LockSupport.unpark(head.next.thread); // 唤醒下一个节点线程
}
3. 节点取消或异常处理(清除队列)
// 前驱节点的 next 被清理
node.prev.next = node.next;
// 后继节点的 prev 被修复
node.next.prev = node.prev;
只有双向链表才能高效完成这些修复工作。
五、为什么不使用其他结构(比如数组、队列类等)?
| 数据结构 | 不适合 AQS 的原因 |
|---|---|
| 数组 | 空间固定、不适合频繁插入删除 |
| 单向链表 | 无法向前回溯修复、取消节点复杂 |
| 队列类(如 ConcurrentLinkedQueue) | 无法定制唤醒逻辑、不支持前驱可控策略 |
| 栈结构 | 不是 FIFO,不符合公平性原则 |
六、相关源码入口
- AQS 类定义:
AbstractQueuedSynchronizer.java - 关键方法:
acquire()/release()addWaiter(Node mode)enq(Node node)unparkSuccessor(Node node)
七、参考资料
- 📘《Java 并发编程实战》:AQS 架构章节
- 🔗 AQS 源码解析(JDK 官方源码)
- 🔗 CLH Queue Wiki
- 📘 《深入理解 Java 虚拟机》并发章节
八、总结口诀
同步阻塞靠排队,线程失败挂链尾;双向结构修节点,唤醒传递最灵活。
更多详细内容请关注其他相关文章!