硬核干货!一口气搞懂 Java AQS

面试必问、源码必看、并发编程绕不开的核心

前言

在 Java 并发编程里,有一个绕不开的名字 ------ AQS

它是 java.util.concurrent 包下大多数并发工具的底层实现。我们常用的 ReentrantLockCountDownLatchSemaphoreCyclicBarrier,全部都是基于 AQS 实现的。

很多读者面试时被问到"讲讲 AQS",要么答不上来,要么答得很零散。今天这篇文章,我带大家彻底搞懂它。


一、AQS 是什么

AQS 全称是 AbstractQueuedSynchronizer,翻译过来就是"抽象队列同步器"。

光看名字可能有点抽象,我们先记住它的核心三要素

  1. 一个状态变量 ------ 用来表示锁是否被占用
  2. 一个队列 ------ 用来存放等待锁的线程
  3. 一套模板方法 ------ 封装了获取锁、释放锁的通用逻辑

AQS 把这些公共逻辑都写好了,子类只需要实现几个关键方法,就能完成一个锁。这就好像我们盖房子,AQS 提供了骨架,我们只需要往里面填砖就能建成。


二、核心结构

我们先看一下 AQS 的核心属性:

java 复制代码
public abstract class AbstractQueuedSynchronizer {
    // 同步状态,0 表示未被占用,>0 表示被占用
    private volatile int state;
    
    // 等待队列的头结点
    private transient Node head;
    
    // 等待队列的尾结点
    private transient Node tail;
}

2.1 等待队列是什么

AQS 内部维护了一个 双向 FIFO 队列,所有竞争锁失败的线程都会被放到这个队列里排队,等待锁释放后被唤醒。

想象一下去银行办业务,只有一个窗口,很多人在门口排队。窗口就是"锁",排队的人就是队列里的 Node。

队列结构大概长这样:

2.2 Node 节点

队列里的每个节点就是一个 Node,包含以下关键字段:

java 复制代码
static final class Node {
    // 节点状态,有以下几种值:
    // CANCELLED = 1      线程已取消等待
    // SIGNAL = -1        后继节点需要被唤醒
    // CONDITION = -2     线程在等待条件
    // PROPAGATE = -3     共享模式下传播
    int waitStatus;
    
    // 前驱和后继节点(双向链表)
    Node prev;
    Node next;
    
    // 关联的线程(这个节点代表哪个线程)
    Thread thread;
    
    // 等待模式:SHARED(共享)或 EXCLUSIVE(独占)
    Node nextWaiter;
}

三、两种同步模式

AQS 支持两种同步模式,理解这一点很关键:

3.1 独占模式

同一时刻只有一个线程 能获取锁。比如我们常见的 ReentrantLock

就像一个厕所只有一个坑位,一次只能进一个人。

3.2 共享模式

同一时刻多个线程 可以同时获取锁。比如 CountDownLatchSemaphore

就像一个停车场有多个车位,可以同时停多辆车。

这就是为什么 ReentrantLock 只能单线程通过,而 CountDownLatch 可以让多个线程同时通过的底层原因。


四、核心方法

AQS 采用模板方法模式 ,定义了一套获取/释放锁的完整流程,子类只需要实现 tryAcquiretryRelease 即可。

4.1 子类需要实现的方法

java 复制代码
// 独占模式:尝试获取锁
// 返回 true 表示获取成功,false 表示失败
protected boolean tryAcquire(int arg)

// 独占模式:尝试释放锁
// 返回 true 表示释放成功
protected boolean tryRelease(int arg)

// 共享模式:尝试获取共享锁
// 返回负数表示失败,0 表示成功但没有剩余资源,正数表示成功且还有剩余资源
protected int tryAcquireShared(int arg)

// 共享模式:尝试释放共享锁
// 返回 true 表示释放成功
protected boolean tryReleaseShared(int arg)

// 检查是否被当前线程独占(用于条件变量的判断)
protected boolean isHeldExclusively()

4.2 模板方法(对外暴露)

这些方法是给外部调用的,内部已经封装好了完整的逻辑:

java 复制代码
// 独占获取(最常用)
public final void acquire(int arg)

// 独占获取,可中断(线程在等待时可以被 interrupt)
public final void acquireInterruptibly(int arg)

// 独占获取,带超时
public final boolean tryAcquireNanos(int arg, long nanosTimeout)

// 独占释放
public final void release(int arg)

// 共享获取
public final void acquireShared(int arg)

// 共享释放
public final void releaseShared(int arg)

五、完整流程分析

5.1 获取锁的全过程

acquire(1) 为例,假设我们要获取一个独占锁,完整流程是这样的:

第一步:tryAcquire 尝试直接获取

调用 tryAcquire(1) 尝试直接获取锁。这个方法是由子类实现的。

  • 如果返回 true,说明获取成功,线程直接持有锁,流程结束
  • 如果返回 false,说明锁被别人占用了,继续往下走

第二步:addWaiter 入队

如果获取失败,需要把当前线程包装成 Node,加入等待队列。

java 复制代码
// 简化的入队逻辑
private Node addWaiter(Node mode) {
    // 把当前线程包装成 Node
    Node node = new Node(Thread.currentThread(), mode);
    
    // 快速 CAS 入队(尝试直接加到队尾)
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 如果快速入队失败,走完整入队流程
    enq(node);
    return node;
}

第三步:acquireQueued 自旋等待

加入队列后,线程并不会一直阻塞,而是先自旋尝试获取几次锁。如果实在获取不到,再阻塞。

为什么要这样设计呢?因为阻塞和唤醒线程需要调用操作系统内核,有一定的开销。如果锁很快释放,自旋等待的效率更高。

java 复制代码
// 简化的自旋等待逻辑
final boolean acquireQueued(Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 获取前驱节点
            Node p = node.predecessor();
            
            // 如果前驱是 head,说明我们是队列第一个等待者
            // 再次尝试获取锁
            if (p == head && tryAcquire(arg)) {
                // 获取成功把自己设置为 head
                setHead(node);
                p.next = null; // 方便 GC
                failed = false;
                return interrupted;
            }
            
            // 获取失败,检查是否需要阻塞
            // 如果前驱节点状态是 SIGNAL,说明可以安全阻塞
            if (shouldParkAfterFailedAcquire(p, node)) {
                // 阻塞当前线程
                LockSupport.park(this);
                // 被唤醒后检查中断状态
                if (Thread.interrupted()) {
                    interrupted = true;
                }
            }
        }
    } finally {
        if (failed) {
            cancelAcquire(node);
        }
    }
}

用一张图来表示整个流程:

flowchart TD A["tryAcquire<br/>尝试直接获取锁"] --> B{"成功?"} B -- 是 --> C["返回<br/>持有锁"] B -- 否 --> D["addWaiter<br/>入队"] D --> E["acquireQueued<br/>自旋等待"] E --> F{"获取成功?"} F -- 是 --> G["返回"] F -- 否 --> H["park()<br/>阻塞"] H --> E

5.2 释放锁的全过程

释放锁相对简单一些:

markdown 复制代码
1. 调用 tryRelease(1) 尝试释放锁
   ├── 失败 → 直接返回
   └── 成功 → 继续

2. 唤醒后继节点(unparkSuccessor)
   └── 后继节点被唤醒后,会继续自旋尝试获取锁

六、实战:手写一个简单锁

理解了 AQS,我们自己来实现一个简单的互斥锁:

java 复制代码
public class SimpleLock extends AbstractQueuedSynchronizer {
    
    // 尝试获取锁
    @Override
    protected boolean tryAcquire(int arg) {
        // CAS 尝试把 state 从 0 改为 1
        // compareAndSetState 是 AQS 提供的原子操作
        return compareAndSetState(0, 1);
    }
    
    // 尝试释放锁
    @Override
    protected boolean tryRelease(int arg) {
        // 直接把 state 设为 0
        setState(0);
        return true;
    }
    
    // 检查是否被当前线程独占
    @Override
    protected boolean isHeldExclusively() {
        return getState() == 1;
    }
    
    // 对外暴露的获取锁方法
    public void lock() {
        acquire(1);
    }
    
    // 对外暴露的释放锁方法
    public void unlock() {
        release(1);
    }
}

测试一下:

java 复制代码
public class TestSimpleLock {
    private static SimpleLock lock = new SimpleLock();
    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " 获取了锁");
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + " 释放了锁");
                }
            }, "线程-" + i).start();
        }
    }
}

输出结果:

复制代码
线程-0 获取了锁
线程-0 释放了锁
线程-1 获取了锁
线程-1 释放了锁
线程-2 获取了锁
线程-2 释放了锁
线程-3 获取了锁
线程-3 释放了锁
线程-4 获取了锁
线程-4 释放了锁

可以看到,线程是依次获取锁的,不会出现并发问题。这就是 AQS 的威力 ------ 才 20 行代码,一个锁就写好了!


七、公平锁 vs 非公平锁

ReentrantLock 本身不实现锁的逻辑,它只是调用 AQS 的模板方法。但它通过传入不同的 Sync 实现类来区分公平和非公平:

  • 非公平锁:插队!直接尝试 CAS 获取锁,不管队列里有没有人在等
  • 公平锁:排队!先看看队列里有没有人,有人就老实在后面排队

7.1 非公平锁

java 复制代码
// 非公平锁的 tryAcquire 实现
protected boolean tryAcquire(int arg) {
    // 直接 CAS 尝试获取,不看队列
    if (compareAndSetState(0, 1)) {
        setExclusiveOwnerThread(Thread.currentThread());
        return true;
    }
    return false;
}

7.2 公平锁

java 复制代码
// 公平锁的 tryAcquire 实现
protected boolean tryAcquire(int arg) {
    // 先看看有没有人在排队
    if (!hasQueuedPredecessors()) {
        // 没人排队,才尝试获取
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
    }
    return false;
}

7.3 怎么选择

优点 缺点
非公平锁 吞吐量高,可能插队成功 可能产生线程饥饿
公平锁 保证先来后到 吞吐量低,需要维护队列

ReentrantLock 默认是非公平的,因为非公平锁的吞吐量通常更高。


八、常见的 AQS 实现类

理解了 AQS,你会发现 JUC 包下大部分并发工具都是基于它实现的:

模式 用途
ReentrantLock 独占 可重入互斥锁
ReentrantReadWriteLock 独占/共享 读写锁,读写分离
CountDownLatch 共享 倒计时,多线程汇合
CyclicBarrier 共享 循环屏障
Semaphore 共享 信号量,限流
Phaser 共享 多阶段同步
相关推荐
止语Lab6 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活6 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
程序员天天困6 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn6 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
用户298698530146 小时前
Python 实现 Excel 与 Markdown 互转的实用指南
后端·python·excel
用户77283104908406 小时前
krono-job:零侵入、单二进制交付的分布式任务调度平台(开源)
后端
Conan在掘金6 小时前
鸿蒙报错速查:Function lacks ending return statement,返回路径漏 return 就炸,根因 + 真解法
后端
人间凡尔赛6 小时前
AI-Native 云原生架构:2026 年从容器编排到智能体编排的范式革命
后端·云原生·架构
IT_陈寒7 小时前
Vue的响应式让我加班到凌晨3点,原来问题出在这
前端·人工智能·后端
卷无止境7 小时前
提升 Python 代码健壮性的方法大盘点
后端·python