硬核干货!一口气搞懂 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 共享 多阶段同步
相关推荐
nbwenren8 小时前
Springboot中SLF4J详解
java·spring boot·后端
helx829 小时前
SpringBoot中自定义Starter
java·spring boot·后端
rleS IONS10 小时前
SpringBoot获取bean的几种方式
java·spring boot·后端
lifewange10 小时前
Go语言-开源编程语言
开发语言·后端·golang
白毛大侠10 小时前
深入理解 Go:用户态和内核态
开发语言·后端·golang
王码码203511 小时前
Go语言中的数据库操作:从sqlx到ORM
后端·golang·go·接口
星辰_mya11 小时前
雪花算法和时区的关系
数据库·后端·面试·架构师
计算机学姐12 小时前
基于SpringBoot的兴趣家教平台系统
java·spring boot·后端·spring·信息可视化·tomcat·intellij-idea
總鑽風12 小时前
单点登录springcloud+mysql
后端·spring·spring cloud
0xDevNull12 小时前
Java 11 新特性概览与实战教程
java·开发语言·后端