并发编程

AQS 独占锁原理详解

深入理解 Java 并发包中 AbstractQueuedSynchronizer 的独占锁实现原理

#AQS#并发##ReentrantLock

AQS

AbstractQueuedSynchronizer (AQS) 是 Java 并发包 (java.util.concurrent) 的 核心基石

简单来说,AQS 是一个用来构建锁和同步器的 框架,JDK 中的 ReentrantLockCountDownLatchSemaphore 等,底层全部依赖 AQS 来管理线程的排队、阻塞和唤醒。


一、 核心原理

AQS 的核心思想可以概括为:状态 (State) + 队列 (Queue) + CAS

它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争抢资源被阻塞时进入此队列)。

1. 核心要素

  • 资源状态 (volatile int state):
    • AQS 使用一个 int 类型的成员变量 state 来表示同步状态。
    • 使用 volatile 保证可见性。
    • 使用 CAS (Compare-And-Swap)state 进行原子修改,保证线程安全。
  • CLH 等待队列:
    • 当线程获取资源(锁)失败时,AQS 会将该线程封装成一个 Node 节点。
    • 这个 Node 会被加入到一个双向链表中(CLH 锁队列的变体)。
    • 队列遵循 FIFO(先进先出)原则。
  • 阻塞与唤醒:
    • AQS 依赖 LockSupport.park() 来阻塞线程(挂起)。
    • 依赖 LockSupport.unpark() 来唤醒线程。

2. 两种资源共享方式

AQS 定义了两种资源共享方式,子类根据需要实现:

  1. Exclusive (独占模式): 只有一个线程能执行,如 ReentrantLock
  2. Shared (共享模式): 多个线程可同时执行,如 SemaphoreCountDownLatch

3. 模板方法模式 (Template Method Pattern)

AQS 使用了模板方法设计模式。AQS 负责把“排队”、“阻塞”、“唤醒”这些复杂的脏活累活都做好了,使用者(子类)只需要重写以下几个简单的方法来定义“什么叫获取成功,什么叫获取失败”:

  • tryAcquire(int): 独占方式尝试获取资源。
  • tryRelease(int): 独占方式尝试释放资源。
  • tryAcquireShared(int): 共享方式尝试获取资源。
  • tryReleaseShared(int): 共享方式尝试释放资源。
  • isHeldExclusively(): 当前线程是否独占资源。

二、 工作流程

ReentrantLock (非公平锁) 为例,AQS 的工作流程如下:

  1. 尝试获取: 线程 A 调用 lock(),底层通过 CAS 尝试将 state 从 0 改为 1。
  2. 成功: 如果成功,设置当前线程为独占线程,直接执行业务代码。
  3. 失败 (入队):
    • 如果线程 B 来了,发现 state 已经是 1 了,CAS 失败。
    • 线程 B 被封装成 Node 节点,追加到 CLH 队列的尾部(tail)。
  4. 阻塞: 线程 B 在队列中自旋检查前驱节点是否是头节点,如果拿不到锁,就调用 LockSupport.park() 让自己沉睡。
  5. 释放与唤醒:
    • 线程 A 业务做完,调用 unlock()
    • state 减为 0。
    • AQS 唤醒队列头部节点的后继节点(即线程 B)。
    • 线程 B 醒来,再次尝试 CAS 获取锁。

三、 实际应用

AQS 在 JDK 中应用极其广泛,不同的同步器对 state 有不同的定义:

同步组件模式state 的含义原理简述
ReentrantLock独占锁的持有次数state=0 表示无锁;state>0 表示重入次数。
释放时需减到 0 才是真正释放。
CountDownLatch共享剩余计数值初始化 state=N
countDown() 也是 CAS 将 state 减 1。
await() 检查如果 state!=0 就入队阻塞。
Semaphore共享剩余许可数量state 代表信号量个数。
acquire()state 减 1,release()state 加 1。
ReentrantReadWriteLock混合高 16 位/低 16 位state 变量被拆分:高 16 位表示 读锁(共享),低 16 位表示 写锁(独占)。

四、概括

AQS 是 Java 并发包中的一个抽象同步框架,核心作用是统一封装线程等待、唤醒和排队机制。

底层用一个 state 变量 + FIFO 等待队列( CLH 变体队列)实现了线程资源的安全抢夺。

volatile state 变量:表示资源状态(比如 0 = 未锁定,1 = 已锁定);

FIFO 等待队列:基于双向链表实现的变体 CLH 队列,每个节点记录线程信息和等待状态;

线程抢不到锁就被加入队列中去排队,AQS 不负责具体锁逻辑(如加锁 / 释放),仅处理线程排队细节,具体锁语义由实现类(如 ReentrantLock、CountDownLatch)自行实现。

AQS 是 Java 并发的 发动机,理解了 AQS,就理解了为什么 Java 的锁比 synchronized 更加灵活(支持超时、中断、公平/非公平)。

  • 关键点: volatile state 维护状态,CAS 保证原子性,CLH 队列 管理排队,LockSupport 控制阻塞。

ReentrantLock 示例解析

NonfairSync(非公平锁)是 ReentrantLock 的默认实现,也是我们在绝大多数场景下推荐使用的模式。

它的核心理念是:极度的实用主义,它并不保证先来后到,而是认为“如果刚好有线程来请求锁,而锁刚好可用,那就直接给它,不用去排队唤醒睡着的线程了”。这能极大地减少线程上下文切换的开销。

下面我们深入源码,看它是如何通过两次“插队”来实现高性能的。


一、核心入口方法

这是非公平锁最“野蛮”的地方。当一个线程调用 lock() 时,它不会像公平锁那样先去问 AQS 队列里有没有人在排队,而是直接尝试抢占。

// ReentrantLock.NonfairSync 中的实现
final void lock() {
    // 1. 第一次插队:直接 CAS 抢锁
    // 也就是常说的:不管三七二十一,先抢一下试试
    if (compareAndSetState(0, 1))
        // 抢到了,将当前线程设为持有者
        setExclusiveOwnerThread(Thread.currentThread());
    else
        // 没抢到,老老实实走 AQS 的标准获取流程
        acquire(1);
}
  • 对比 FairSync: 公平锁的 lock() 方法里没有这个 if 判断,它直接调用 acquire(1),完全遵循排队规则。
  • 如果锁刚好被释放,新来的线程不需要经历“入队 -> 阻塞 -> 唤醒”的昂贵过程,直接就能拿到锁执行,这就是插队。

二、AQS 回调

lock() 中的第一次 CAS 失败后,代码进入 acquire(1),AQS 的 acquire 模板方法会调用子类实现的 tryAcquire

NonfairSync 中,tryAcquire 直接调用了 nonfairTryAcquire

这里发生了 第二次插队

// ReentrantLock.Sync (父类中定义,NonfairSync 直接调用)
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState(); // 获取当前锁的状态

    // --- 场景 1: 锁当前是空闲的 (state == 0) ---
    if (c == 0) {
        // 2. 第二次插队
        // 即使队列里有人在排队,我也不管,我只要能 CAS 成功,这锁就是我的
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // --- 场景 2: 锁已经被持有了,但持有者是自己 (重入逻辑) ---
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires; // 增加重入次数
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        
        // 重点:这里不需要 CAS!
        // 因为已经持有锁了,其他线程进不来,没有并发竞争,直接赋值即可
        setState(nextc); 
        return true;
    }
    
    // --- 场景 3: 锁被别人拿着 ---
    return false; // 返回 false,AQS 会把当前线程通过 addWaiter 加入队列
}

关键源码细节解析:

  1. 为什么叫 Nonfair (非公平)?
    • c == 0 的判断块中,它 没有 去检查 AQS 队列中是否有前驱节点(即没有调用 hasQueuedPredecessors())。
    • 只要当前锁是空的,它就尝试 CAS。这意味着:一个刚刚到达的线程,可能会抢在已经在队列里等了很久的线程前面拿到锁
  2. 重入 (Reentrancy) 的实现
    • 通过 current == getExclusiveOwnerThread() 判断。
    • 重入仅仅是 state + 1
    • 释放锁时,必须 state 减到 0 才会真正释放给其他线程。

三、对比 FairSync (公平锁)

为了更清晰地理解 NonfairSync 的“流氓”行径,看一眼 FairSynctryAcquire 区别:

// FairSync 的实现
protected final boolean tryAcquire(int acquires) {
    // ... 省略部分代码 ...
    if (c == 0) {
        // ! hasQueuedPredecessors() 是核心区别!
        // 只有当“队列为空”或者“我是队列里的第一个”时,才允许 CAS
        if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ...
}
  • FairSync: 讲究先来后到,不仅看 state,还要看队列。
  • NonfairSync: 讲究效率优先,只看 state,不看队列。

四、为什么要“非公平”?

你可能会问,非公平锁会不会导致队列里的线程一直拿不到锁(饥饿现象)?

理论上会,但实际中极少发生,Java 选择非公平锁作为默认值,主要基于性能考量:

  1. 减少唤醒开销: 恢复一个被挂起(Parked)的线程需要从内核态切回用户态,这个过程很慢。

  2. 利用时间差: 假设线程 A 释放锁,唤醒线程 B,在 B 还没完全醒过来并被调度执行之前,线程 C 刚好请求锁。

    • 非公平模式: C 抢到了锁,迅速执行完并释放,此时 B 刚好醒过来,拿到锁。这样 A、B、C 都没有浪费 CPU 时间,吞吐量极高。

    • 公平模式: C 只能入队等待,CPU 在 B 醒来之前可能处于空闲状态,浪费了算力。

源码核心:

NonfairSynclock() 入口处和 tryAcquire() 逻辑中,两次 忽略队列情况直接尝试 CAS 修改 state,只有这两次都失败了,才会乖乖去 AQS 队列里排队。


五、动画演示

下面用 ReentrantLock非公平锁 的动画展示了 AQS 独占锁的完整竞争过程

Tip

这里以 Thread A、B、C 三个线程的锁竞争、入队、阻塞和唤醒流程作为演示,你可以清晰地看到整个锁竞争的过程,包括队列的懒加载初始化、节点的 waitStatus 变化、以及线程的阻塞与唤醒机制

AQS 独占锁竞争全过程

ReentrantLock (Nonfair) 动态模拟
0/13
State (锁状态)
0
Owner (持有者)
null
Head (头节点)
null
Tail (尾节点)
null
无锁竞争,队列为空
Phase 0: 初始状态
系统启动,State=0Owner=null
此时内存中没有任何 Node,队列是 懒加载 (Lazy Loading) 的。

Condition 示例解析

Condition (条件队列) 是 AQS 中非常精彩的一部分,它配合锁实现了线程间的 精确等待与唤醒

如果说 state 和 AQS 队列解决了“怎么抢锁”的问题,那么 Condition 就解决了“抢到锁了,但条件不满足,怎么先歇会儿(释放锁)等条件满足了再回来”的问题。

它是 Object.wait() / Object.notify() 的升级版,最大的优势在于 支持多个等待队列(One Lock, Multiple Conditions)。

一、核心概念

理解 Condition 的关键在于脑海中要有 两个队列 的概念:

  1. 同步队列 (Sync Queue / AQS Queue): 也就是前面提到的 CLH 队列,这里的线程都在 排队抢锁
  2. 条件队列 (Condition Queue): 这是 ConditionObject 内部维护的一个单向链表,这里的线程都在 等待某个条件(比如队列不满、金额足够),它们 不持有锁,也不在抢锁,而是处于“挂起”状态。

Condition 机制的本质,就是节点在这两个队列之间的“大迁徙”。


二、核心结构

ConditionObject 是 AQS 的内部类。

public class ConditionObject implements Condition, java.io.Serializable {
    private transient Node firstWaiter; // 条件队列的头节点
    private transient Node lastWaiter;  // 条件队列的尾节点
    // ...
}
  • 复用 Node: 它复用了 AQS 的 Node 类,但使用的是 nextWaiter 属性将节点串成单向链表。
  • 状态标记: 处于条件队列中的节点,其 waitStatus 被标记为 Node.CONDITION (-2)。

三、核心流程源码解析

流程图

AQS-Condition

我们以最经典的“生产者-消费者”场景为例:线程 A 调用 await() 等待,线程 B 调用 signal() 唤醒。

1. await():从“同步队列”迁移到“条件队列”

当线程 A 拿到锁后,发现条件不满足(比如队列满了,我是生产者),调用 condition.await()

public final void await() throws InterruptedException {
    if (Thread.interrupted()) throw new InterruptedException();
    
    // 1.把自己封装成 Node,加入到【条件队列】的队尾
    Node node = addConditionWaiter(); 
    
    // 2.【彻底释放锁】
    // 注意:await 是会释放锁的,而且是 fullyRelease(不管重入了多少次,一次性减到 0)
    // 返回值 savedState 是为了将来被唤醒重新拿锁时,能恢复重入次数
    int savedState = fullyRelease(node);
    
    int interruptMode = 0;
    
    // 3. 阻塞挂起
    // 只要该节点还在条件队列中(没被移动到 Sync 队列),就一直 park 睡大觉
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    
    // 4. 被 signal 唤醒后,进入 acquireQueued 重新在【同步队列】里抢锁
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // ...
}

关键动作: 持有锁的线程 -> 构造节点入条件队列 -> 释放锁 -> LockSupport.park()

2. signal():从“条件队列”迁移回“同步队列”

线程 B 拿到锁,消费了一个数据,队列不满了,调用 condition.signal() 通知线程 A。

public final void signal() {
    // 必须持有独占锁才能 signal,否则抛异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
        
    Node first = firstWaiter;
    if (first != null)
        doSignal(first); // 唤醒条件队列里的第一个家伙
}

// 核心逻辑在 doSignal 调用的 transferForSignal 中
final boolean transferForSignal(Node node) {
    // 1. 修改状态:从 CONDITION (-2) 改为 0 (初始状态)
    // 如果失败说明该节点可能已经取消等待了
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    // 2. 【关键一步】:将节点从【条件队列】转移到【同步队列】(enq)
    // p 是该节点在同步队列中的前驱节点
    Node p = enq(node); 
    
    int ws = p.waitStatus;
    // 3. 如果前驱节点取消了,或者尝试设置前驱节点为 SIGNAL 失败
    // 则直接 unpark 唤醒当前线程,让它去 acquireQueued 里自旋修正
    // 通常情况下,这里只做转移,不立即唤醒,等线程 B unlock 的时候自然会唤醒 A
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
        
    return true;
}

关键动作: 摘下条件队列头节点 -> 修改状态 -> enq() 插入同步队列尾部。

Warning

注意: signal() 并不等于“立刻唤醒线程运行”,它只是把线程从“等待室”(条件队列)领到了“候诊室”(同步队列)。

线程 A 此时还在排队,只有等线程 B 调用 unlock() 真正释放锁后,A 才有机会抢到锁并从 await() 中返回。


四、动画演示

同样的,这里再提供一个动画,你可以一步一步感受线程是如何在 AQS 的同步队列与条件队列之间互相流转的。

Tip

推荐这样操作:

  1. 先点击 “加锁” 按钮,进入抢锁流程
  2. 再点击 “await” 按钮,我这里会同时加入新的线程并抢到锁,观察线程的变化
  3. 可以多点击两三次 “await” 按钮,多让几个线程都进入条件队列中,方便观看
  4. 后可以再多点击两三次 “signal” 按钮,让线程从条件队列中出来,进入同步队列中
  5. 最后点击 “unlock” 按钮,让当前线程释放锁,同步队列的其他线程去抢锁

AQS 核心机制

AbstractQueuedSynchronizer

同步状态

State Engine

0
volatile int state
👨‍🚀
Exclusive Owner无持有者
双向链表 (CLH)

同步队列 Sync Queue

0 个节点阻塞
队列为空 (Empty)
单向链表

条件队列 Condition Queue

0 个线程等待 (ws: -2)
无等待线程 (Empty)
ConditionObject.java
public final void await() {
  Node node = addConditionWaiter();
  int savedState = fullyRelease(node);
  while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
  }
}

public final void signal() {
  if (firstWaiter != null)
    doSignal(firstWaiter);
}

final boolean transferForSignal(Node n) {
  // 1. WS: -2 -> 0
  casWaitStatus(n, -2, 0);

  // 2. Enq Sync Queue
  enq(n);
}

系统日志 Log

7:42:01 AMINFO
系统初始化完成,State = 0

五、实际应用 ArrayBlockingQueue

JDK 中的阻塞队列 ArrayBlockingQueue 是 Condition 最教科书式的应用,它使用了 一把锁 + 两个条件

final ReentrantLock lock;
final Condition notEmpty; // 消费者等待队列:没得吃了,等不空
final Condition notFull;  // 生产者等待队列:吃撑了,等不满

public void put(E e) throws InterruptedException {
    lock.lock();
    try {
        while (count == items.length)
            notFull.await(); // 满了,在 notFull 条件上等待
        enqueue(e);
        // 生产完了一个,队列肯定不空了,喊醒消费者
        notEmpty.signal(); 
    } finally {
        lock.unlock();
    }
}

public E take() throws InterruptedException {
    lock.lock();
    try {
        while (count == 0)
            notEmpty.await(); // 空了,在 notEmpty 条件上等待
        E x = dequeue();
        // 消费完了一个,队列肯定不满了,喊醒生产者
        notFull.signal();
        return x;
    } finally {
        lock.unlock();
    }
}

六、总结

  1. 数据结构: Condition 内部维护一个单向链表。
  2. await: 释放锁 -> 进条件队列 -> 阻塞。
  3. signal: 出条件队列 -> 进同步队列(AQS 队列) -> 准备抢锁。
  4. 优势: 相比 Object.wait/notify 只能混在一个等待池里,Condition 可以将等待线程按“原因”分组(比如 notFullnotEmpty),从而避免“惊群效应”或“唤醒了错误的线程”,效率更高。

Tip

小问题思考: 为什么 await() 方法中,调用 addConditionWaiter() 加入条件队列不需要 CAS,而 ReentrantLockAQS 队列 需要 CAS 呢?

因为调用 await 时线程肯定已经持有锁了,是线程安全的;而入 AQS 队列时线程还没拿到锁,存在竞争。