【Java并发】AQS详解:JUC包背后的“幕后大佬”

在 Java 并发编程(JUC)的世界里,我们经常使用 ReentrantLockCountDownLatchSemaphore 这些赫赫有名的工具类。

但你是否想过,这些功能各异的工具背后,其实共用着同一套“底盘”?

这就是我们今天要聊的主角——AQS (AbstractQueuedSynchronizer,抽象队列同步器)。它是 JUC 包的心脏,掌握了它,你就掌握了 Java 并发的半壁江山。

一、 什么是 AQS?

AQS 是一个用于构建锁和同步器的框架。

如果把 ReentrantLockCountDownLatch 等比作是成品的汽车(跑车、卡车、公交车),那么 AQS 就是通用的汽车底盘和引擎

  • AQS 负责脏活累活:它处理了线程的排队、阻塞、唤醒、线程安全等最复杂的底层逻辑。
  • 同步器负责业务逻辑:具体的工具类只需要告诉 AQS,“什么时候算获取锁成功”,“资源一共有多少”,剩下的交给 AQS 即可。

简单来说,AQS 是一个“原材料”,我们可以根据它加工出各种各样的同步器。

二、 AQS 的核心架构

AQS 的内部并没有多么神秘,它的核心逻辑主要由三部分组成:

1. 核心资源:State (volatile int)

AQS 内部维护了一个 state 变量,用来表示“同步状态”

  • 这是一个 volatile 修饰的 int 变量,保证了多线程下的可见性。
  • AQS 提供了 CAS (Compare And Swap) 方法来原子性地修改这个值。

这个 state 具体代表什么?AQS 不关心,由子类定义:

  • ReentrantLock 中,state=0 代表没锁,state=1 代表上锁。
  • CountDownLatch 中,state 代表倒计时的数值。
  • Semaphore 中,state 代表剩余的许可证数量。

2. 等待队列:CLH 队列

如果有线程想抢资源(修改 state)失败了怎么办?总不能让它一直死循环空转(自旋)吧?

AQS 会把这些竞争失败的线程包装成一个 Node 节点,加入到一个双向链表(CLH队列)的尾部,并将线程阻塞(Park)。等待持有资源的线程释放后,再唤醒队列头部的线程。

3. 阻塞与唤醒工具

AQS 底层利用 LockSupport 类中的 park()unpark() 方法,真正实现了操作系统的线程挂起和唤醒。


三、 AQS 是如何工作的?(以抢锁为例)

我们可以把 AQS 的工作流程想象成“去银行柜台办理业务”

  1. 尝试获取 (TryAcquire)
    • 线程 A 来了,看到柜台窗口(State)是空的(0),立马坐下办理,把窗口状态改为“忙碌”(1)。
  2. 入队 (Enqueue)
    • 线程 B 来了,发现窗口状态是“忙碌”(1)。
    • AQS 这位大堂经理就会把线程 B 领到旁边的等待区(CLH 队列),让它排在队伍后面,并告诉它:“你先睡会儿(Block),轮到你了叫你”。
  3. 释放与唤醒 (Release & Unpark)
    • 线程 A 办完业务了,离开柜台,把窗口状态改回“空闲”(0)。
    • AQS 大堂经理发现窗口空了,就去等待区叫醒排在最前面的线程 B:“醒醒,轮到你了”。
    • 线程 B 醒来,高高兴兴去柜台办理业务。

四、 AQS 的设计模式:模板方法模式

AQS 之所以强大,是因为它使用了模板方法设计模式

AQS 把“排队”、“阻塞”、“唤醒”这些通用的逻辑都写死了(acquirerelease 方法)。但是,“如何判断资源是否足够”“如何修改 state” 这些逻辑是留给子类去实现的。

作为开发者,我们如果想自定义一个锁,只需要继承 AQS 并重写以下几个“钩子方法”:

  • tryAcquire(int): 独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
  • tryRelease(int): 独占方式。尝试释放资源。
  • tryAcquireShared(int): 共享方式。尝试获取资源。
  • tryReleaseShared(int): 共享方式。尝试释放资源。
  • isHeldExclusively(): 该线程是否正在独占资源。

五、 AQS 的“成品”展示

让我们看看 Java 大佬们是如何利用 AQS 这个“原材料”加工出著名工具类的:

1. ReentrantLock(可重入锁)

  • 模式:独占模式。
  • 加工逻辑
    • state 初始化为 0。
    • 线程 A 抢锁,通过 CAS 把 state 变成 1。
    • 如果是同一个线程 A 再次抢锁(重入),就把 state 加 1(变成 2, 3…)。
    • 释放锁时,state 减 1,直到减为 0 时才真正释放。

2. CountDownLatch(倒计时器)

  • 模式:共享模式。
  • 加工逻辑
    • state 初始化为 N(比如 5)。
    • 主线程调用 await(),如果 state > 0,就进入队列等待。
    • 其他线程调用 countDown(),通过 CAS 把 state 减 1。
    • state 减到 0 时,AQS 唤醒队列里所有等待的主线程。

3. Semaphore(信号量)

  • 模式:共享模式。
  • 加工逻辑
    • state 初始化为许可证数量(比如 3)。
    • 线程来获取许可,state 减 1。
    • 如果 state 变为负数或者不足,线程进入队列等待。
    • 释放许可时,state 加 1。

六、 总结

AQS 是 Java 并发编程的基石。

  • 宏观上:它是一个框架,通过State(资源状态)CLH Queue(等待队列)来管理线程的同步。
  • 设计上:它采用了模板方法模式,将复杂的队列管理封装起来,只暴露简单的状态修改接口给子类。
  • 应用上:它是 ReentrantLockSemaphoreCountDownLatch 等工具类的父类(的内部实现)。

理解了 AQS,你就理解了 Java 锁机制的灵魂。下次面试官问你:“ReentrantLock 是怎么实现的?”你就可以自信地从 AQS 的 state 和队列讲起了!

AQS的具体原理可以参考: https://javaguide.cn/java/concurrent/aqs.html


希望这篇文章能帮你彻底搞懂 AQS!如果有帮助,欢迎点赞收藏。