AbstractQueuedSynchronized为什么采用双向链表
                           
天天向上
发布: 2025-07-12 13:48:11

原创
330 人浏览过

AbstractQueuedSynchronizer(简称 AQS)是 Java 并发包(java.util.concurrent.locks)中最核心的同步框架之一,如 ReentrantLockSemaphoreCountDownLatchFutureTask 等都基于它实现。

在 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,不符合公平性原则

六、相关源码入口


七、参考资料


八、总结口诀

同步阻塞靠排队,线程失败挂链尾;双向结构修节点,唤醒传递最灵活。


更多详细内容请关注其他相关文章!

发表回复 0

Your email address will not be published. Required fields are marked *