深入理解 AbstractQueuedSynchronizer(AQS):构建高性能同步器的基石

AbstractQueuedSynchronizer(简称 AQS)是 Java 并发包(java.util.concurrent)中最核心的基础类之一,几乎所有自定义的同步器(如 ReentrantLockCountDownLatchSemaphoreFutureTask 等)都直接或间接地基于 AQS 实现。

本文将深入探讨 AQS 的核心设计思想、内部数据结构、关键方法实现、以及它背后的并发控制模型。


🧠 一、AQS 的核心设计思想

AQS 提供了一个 队列式的、基于状态的同步框架,它的设计思想非常明确:

🌟 基本模型:

通过维护一个 volatile 的 state 变量来表示同步状态,并用一个 FIFO 队列管理等待线程。

具体来说,它实现了两种锁的语义:

  • 独占(Exclusive) :只能一个线程获取,比如 ReentrantLock
  • 共享(Shared) :多个线程可以同时获取,比如 Semaphore

开发者通过继承 AQS 并重写其 tryAcquire / tryRelease 等方法来实现自定义的同步逻辑,而不需要关心线程挂起/唤醒的底层细节。


🏗️ 二、AQS 的关键内部结构

1. state 变量(同步状态)

java 复制代码
private volatile int state;

这是 AQS 的核心状态变量,通常用来表示资源是否可用、计数器大小等。子类通过 getState() / setState() / compareAndSetState() 来访问和修改它。


2. CLH 队列(等待队列)

AQS 使用一种变种的 CLH(Craig, Landin, and Hagersten)锁队列 来维护所有正在等待获取锁的线程。

java 复制代码
static final class Node {
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    volatile int waitStatus;
}

常见的 waitStatus 值:

  • 0:默认状态
  • SIGNAL (-1):后继节点等待唤醒
  • CANCELLED (1):线程取消等待
  • CONDITION (-2):表示在 condition 上等待
  • PROPAGATE (-3):共享模式传播

队列头尾指针:

java 复制代码
private transient volatile Node head;
private transient volatile Node tail;

AQS 通过 enq()acquireQueued() 等方法管理队列入队/出队与线程阻塞/唤醒的逻辑。


⚙️ 三、核心方法详解

1. acquire(int arg):独占模式获取

java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

流程如下:

  • 首先尝试获取锁(调用子类实现的 tryAcquire());
  • 获取失败则将当前线程包装成 Node 并加入等待队列;
  • 在队列中自旋等待,并最终阻塞(LockSupport.park());
  • 直到被前驱唤醒并重新竞争资源。

2. release(int arg):独占模式释放

java 复制代码
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}
  • 调用子类的 tryRelease() 释放资源;
  • 如果成功,唤醒等待队列中的下一个线程。

3. acquireShared(int arg) / releaseShared(int arg)

共享模式下的获取和释放,允许多个线程同时访问资源(如信号量)。

java 复制代码
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

🎯 四、自定义同步器的三大步骤

要实现一个自定义同步器,一般遵循以下步骤:

1. 继承 AbstractQueuedSynchronizer

java 复制代码
class MyLock extends AbstractQueuedSynchronizer {
    ...
}

2. 实现同步逻辑(重写关键方法)

java 复制代码
protected boolean tryAcquire(int arg) {
    // 例如实现不可重入独占锁
    return compareAndSetState(0, 1);
}

protected boolean tryRelease(int arg) {
    setState(0);
    return true;
}

3. 暴露锁 API 给外部调用

java 复制代码
public void lock() {
    acquire(1);
}

public void unlock() {
    release(1);
}

💡 五、AQS 的典型应用场景

类名 功能 AQS 模式
ReentrantLock 可重入独占锁 独占
Semaphore 信号量 共享
CountDownLatch 倒计时器 共享
ReadWriteLock 读写锁 独占 + 共享
FutureTask 任务状态管理 独占

这些类都封装了 AQS 提供的同步能力,让我们得以用更高层的 API 进行线程协作。


🔍 六、深入理解:CLH 队列与线程挂起/唤醒

✴️ 挂起线程:LockSupport.park()

当线程获取锁失败,就会调用 LockSupport.park(this) 挂起自己。

✴️ 唤醒线程:LockSupport.unpark(thread)

释放锁后,会唤醒队列中的下一个节点对应的线程。

这是一种 显式挂起-唤醒机制 ,相比传统 wait/notify 更加灵活和安全,不依赖对象 monitor。


🧪 七、AQS 存在的问题和挑战

  • 非公平锁可能导致线程"饿死"
  • 锁竞争激烈时,自旋+挂起开销大
  • 调试困难,出错后难以定位问题源头
  • 死锁风险高 ,一旦 tryAcquire 实现不当容易出问题;

因此,在使用 AQS 时,强烈建议封装为高层 API 使用,不要轻易手写同步器,除非非常熟悉。


✅ 总结

特性 描述
模型 基于状态的同步器
队列 CLH 队列实现等待线程排队
支持 独占与共享两种模式
控制点 子类重写 tryAcquire / tryRelease 等控制同步行为
应用 ReentrantLock、Semaphore、CountDownLatch、FutureTask 等

📘 推荐阅读与资料

  • 《Java 并发编程实战》(Java Concurrency in Practice)
  • Doug Lea 的 AQS 原始设计文档
  • JDK 源码中的 AbstractQueuedSynchronizer.java 注释
  • 博客:Martin Fowler 的并发模式分析

相关推荐
程序员莫小特2 小时前
老题新解|求三角形面积
开发语言·数据结构·c++·算法·信息学奥赛一本通
mc23562 小时前
C语言指针详解
c语言·开发语言·算法
静渊谋2 小时前
攻防世界-Check
java·安全·网络安全
兰亭妙微3 小时前
兰亭妙微桌面端界面设计优化方案:避免QT开发中的“老旧感”
开发语言·qt·ui·用户体验设计公司·ui设计公司
KL41803 小时前
[QT]常用控件一
开发语言·c++·qt
代码充电宝3 小时前
LeetCode 算法题【简单】49. 字母异位词分组
java·算法·leetcode·面试·哈希算法
摸鱼的老谭3 小时前
Java学习之旅第一季-28:Java基础语法测试题
java·java基础测试
大翻哥哥3 小时前
Python 2025:异步革命与AI驱动下的开发新范式
开发语言·人工智能·python
mark-puls3 小时前
Qt标签页控件QTabWidget全面指南:创建现代化多页界面
开发语言·qt·设计模式