AQS 是什么?它存在什么问题?

一则或许对你有用的小广告

欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论

  • 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于 Spring AI + Spring Boot3.x + JDK 21...点击查看;
  • 《从零手撸:仿小红书(微服务架构)》 已完结,基于 Spring Cloud Alibaba + Spring Boot3.x + JDK 17...点击查看项目介绍; 演示链接: http://116.62.199.48:7070/;
  • 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/

面试考察点

面试官提出这个问题,通常旨在考察:

  1. 对 Java 并发包底层核心机制的掌握程度:不仅仅是了解 ReentrantLockCountDownLatch 等工具的使用,更是想知道它们共同的实现基石是什么。
  2. 对框架设计与设计模式的理解:AQS 是“模板方法模式”的经典应用,面试官想看你是否能洞察其设计精髓。
  3. 深入源码分析和批判性思维的能力:你是否能在理解其强大之处的同事,也看到其历史局限性和设计上的权衡,这体现了你的技术深度和视野。
  4. 解决实际问题的经验:你是否曾因 AQS 的问题而 “踩坑”,或了解其相关的最佳实践与替代方案。

核心答案

AQS(AbstractQueuedSynchronizer,抽象队列同步器) 是 Java 并发包(java.util.concurrent.locks)中一个构建锁和同步器的核心底层框架。像 ReentrantLockSemaphoreCountDownLatch 等都是基于它实现的。它通过一个整型的 volatile 状态变量(state 和一个 FIFO 线程等待队列(CLH 变体) 来管理资源的获取与释放。

AQS 主要存在的问题有:

  1. API 设计较为复杂且易误用:其 protected 方法(如 tryAcquire)需要子类精确实现,容易因逻辑错误导致死锁或性能问题。
  2. 默认的非公平策略可能导致 “线程饥饿”:虽然性能通常更好,但在高竞争下,新来的线程可能一直抢占资源,导致队列中的线程长期等待。
  3. 功能扩展门槛高:正确、高效地实现一个自定义同步器需要对 AQS 有非常深入的理解,对于普通开发者难度较大。
  4. 无法很好地支持更丰富的同步模式:例如,ReadWriteLock 基于 AQS 的实现(ReentrantReadWriteLock)在 “读多写少” 场景下,写锁可能因大量读线程而长期饥饿,这是其模型决定的局限性。

深度解析

原理/机制

AQS 的核心思想是,将资源共享状态的管理和线程排队等待的机制解耦。

  • 状态管理(state:这是一个 volatile int 变量,其含义由子类定义。例如,在 ReentrantLock 中,state=0 表示锁空闲,state>0 表示被持有且可重入次数;在 Semaphore 中,state 表示剩余的许可证数量。
  • 线程排队(CLH 队列):这是一个双向链表(在 JDK 1.6 后)构成的 FIFO 队列,用于存放获取资源失败的线程。AQS 通过 CAS(Compare-And-Swap) 操作来保证入队、出队的线程安全。
  • 模板方法模式:AQS 定义了 acquire()release() 等核心公共方法,这些方法会调用子类必须实现的 tryAcquire(int arg)tryRelease(int arg) 等钩子方法。子类只需关注 “如何尝试获取/释放资源” 这一特定逻辑,而 “如何排队、阻塞/唤醒线程” 这些复杂且通用的部分则由 AQS 框架完成。

代码示例

我们来看一个基于 AQS 实现的最简独占锁(不可重入),它清晰地展示了如何与 AQS 协作:

// 一个非常简单的、基于AQS的不可重入互斥锁
public class SimpleMutexLock {
    private static final class Sync extends AbstractQueuedSynchronizer {
        // 尝试获取锁:当状态为0时,通过CAS将其设为1,表示获取成功
        @Override
        protected boolean tryAcquire(int acquires) {
            assert acquires == 1; // 这里只支持获取1个资源
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
        // 尝试释放锁:将状态从1设回0
        @Override
        protected boolean tryRelease(int releases) {
            assert releases == 1;
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            setExclusiveOwnerThread(null);
            setState(0); // 此处无需CAS,因为只有持有锁的线程才能调用释放
            return true;
        }
    }

    private final Sync sync = new Sync();
    public void lock()   { sync.acquire(1); }
    public void unlock() { sync.release(1); }
}

对比分析与最佳实践

  • AQS vs. synchronized:AQS 提供了更灵活、功能更强的同步控制(如可中断、超时、公平性选择、条件变量),但 synchronized 随着 JVM 优化(锁升级),在低竞争场景下性能已非常好且写法更简洁。
  • 公平 vs. 非公平ReentrantLock 可以选择公平或非公平策略。非公平锁吞吐量高,但可能饿死线程;公平锁保证了顺序,但上下文切换频繁,吞吐量可能下降 20%-30%在实践中的默认选择通常是非公平锁
  • 最佳实践
    1. 优先使用 JUC 包中的高级工具(如 ReentrantLock, Semaphore),而非直接继承 AQS 造轮子。
    2. 需要自定义复杂同步逻辑时,务必吃透 AQS 的 state 管理和队列行为,并编写详尽的单元测试。
    3. 对于读多写少的场景,可以考虑 StampedLock(乐观读)或 ReentrantReadWriteLock(但要注意写锁饥饿问题),并做压测对比。

常见误区

  • 误区一:认为 AQS 队列是绝对的公平。实际上,tryAcquire 在入队前会尝试一次直接获取(非公平抢锁),这是其高性能的关键之一。
  • 误区二:混淆 state 的语义。state 是 AQS 框架提供的资源计数工具,其具体含义完全由子类实现决定
  • 误区三:认为基于 AQS 的锁一定比 synchronized 快。在低并发场景下,synchronized(偏向锁、轻量级锁)的性能开销可能更小。

总结

AQS 是 JUC 并发大厦的基石,通过 “状态变量 + FIFO 队列” 和 “模板方法模式” 优雅地统一了同步器的实现范式;然而,其复杂的 API、潜在的非公平性以及在某些同步模式下的局限性,是开发者在高级应用时需要清醒认识的问题。