硬核干货!一口气搞懂 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
尝试直接获取锁"] --> B{"成功?"} B -- 是 --> C["返回
持有锁"] B -- 否 --> D["addWaiter
入队"] D --> E["acquireQueued
自旋等待"] E --> F{"获取成功?"} F -- 是 --> G["返回"] F -- 否 --> H["park()
阻塞"] 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 共享 多阶段同步
相关推荐
初次攀爬者1 小时前
Spring中Bean的生命周期
后端·spring
PPPPickup2 小时前
深信服公司---java实习生后端一二面询问
java·后端·ai
架构师沉默2 小时前
为什么很多大厂 API 不再使用 PUT 和 DELETE?
java·后端·架构
回家路上绕了弯2 小时前
Claude Code Agent Team 全解析:AI 集群协作,重构代码开发新范式
人工智能·分布式·后端
树獭叔叔3 小时前
扩散模型完全指南:从直觉到数学的深度解析
后端·aigc·openai
毕设源码_严学姐3 小时前
计算机毕业设计springboot心理健康辅导系统 高校学生心灵关怀服务平台的设计与实现 校园智慧心理服务系统的设计与实现
spring boot·后端·课程设计
程序员牛奶3 小时前
如何使用Redis Set实现简单的抽奖系统?
后端
程序员海军3 小时前
深度测评:在微信里直接操控 OpenClaw
人工智能·后端
野犬寒鸦3 小时前
面试常问:HTTP 1.0 VS HTTP 2.0 VS HTTP 3.0 的核心区别及底层实现逻辑
服务器·开发语言·网络·后端·面试