深入解析 Java AQS (AbstractQueuedSynchronizer) 的实现原理

发布于:2025-07-17 ⋅ 阅读:(19) ⋅ 点赞:(0)

1. 引言

Java 并发包 (java.util.concurrent.locks) 中的 ReentrantLockSemaphoreCountDownLatch 等同步工具的核心实现都依赖于 AQS (AbstractQueuedSynchronizer)。AQS 提供了一套通用的同步框架,通过 FIFO 队列 管理线程的排队与唤醒,并支持 独占锁共享锁 两种模式。

本文将深入分析 AQS 的实现原理,包括:

  • AQS 的核心数据结构
  • 同步队列(CLH 变体)的实现
  • 线程的入队、阻塞、唤醒机制
  • 公平锁与非公平锁的实现差异
  • AQS 的模板方法设计
  • 关键源码分析 + 流程图

2. AQS 的核心结构

AQS 的核心依赖以下几个关键组件:

  1. volatile int state:同步状态,不同的锁有不同的语义:
    • ReentrantLockstate 表示锁的重入次数(0 表示未锁定,1 表示被占用,>1 表示重入)。
    • Semaphorestate 表示剩余的许可数量。
    • CountDownLatchstate 表示剩余的计数。
  2. 双向同步队列(CLH 变体):存储等待获取锁的线程。
  3. Node 节点:封装线程和等待状态。

2.1 AQS 同步队列结构

AQS 使用 双向链表 实现同步队列,每个节点是一个 Node 对象:

static final class Node {
    volatile int waitStatus;  // 节点状态(CANCELLED, SIGNAL, CONDITION, PROPAGATE)
    volatile Node prev;       // 前驱节点
    volatile Node next;       // 后继节点
    volatile Thread thread;   // 关联的线程
    Node nextWaiter;          // 用于条件队列或共享模式
}

waitStatus 的取值:

  • CANCELLED (1):线程已取消等待(超时或中断)。
  • SIGNAL (-1):当前节点的后继节点需要被唤醒。
  • CONDITION (-2):节点在条件队列中(如 Condition.await())。
  • PROPAGATE (-3):共享模式下,唤醒需要传播。

headtail

  • head:虚拟头节点,不存储线程,仅用于管理队列。
  • tail:指向队列的最后一个节点。

同步队列示例:

head (dummy) → Node(thread1, SIGNAL) → Node(thread2, SIGNAL) → tail

3. AQS 的关键流程

3.1 获取锁(acquire)

ReentrantLock 为例,调用 lock() 最终会进入 AQS 的 acquire(1)

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&      // 尝试获取锁(子类实现)
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 失败则入队
        selfInterrupt();          // 如果被中断,恢复中断状态
}

流程详解:

  1. tryAcquire(arg)(子类实现):
    • 非公平锁:直接尝试 CAS 修改 state,成功则获取锁。
    • 公平锁:先检查是否有前驱节点 (hasQueuedPredecessors()),再尝试 CAS。
  2. addWaiter(Node.EXCLUSIVE)
    • 将当前线程包装成 Node,CAS 插入队尾。
  3. acquireQueued(node, arg)
    • 自旋检查前驱是否是 head(即自己是第一个等待者)。
    • 如果是,再次尝试 tryAcquire
    • 如果不是,调用 shouldParkAfterFailedAcquire() 判断是否需要阻塞:
      • 前驱 waitStatus=SIGNAL → 可以安全阻塞(前驱释放时会唤醒自己)。
      • 前驱 waitStatus=CANCELLED → 跳过取消的节点。
    • 调用 LockSupport.park() 阻塞线程。

流程图:

graph TD
    A[线程调用 lock()] --> B{tryAcquire 成功?}
    B -->|是| C[获取锁, 直接执行]
    B -->|否| D[addWaiter 入队]
    D --> E[acquireQueued 自旋检查]
    E --> F{前驱是 head?}
    F -->|是| G[tryAcquire 尝试获取锁]
    G -->|成功| H[设置自己为 head, 执行]
    G -->|失败| I[shouldParkAfterFailedAcquire]
    F -->|否| I
    I --> J[LockSupport.park 阻塞]

3.2 释放锁(release)

调用 unlock() 最终进入 release(1)

public final boolean release(int arg) {
    if (tryRelease(arg)) {       // 子类实现释放逻辑
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);   // 唤醒后继节点
        return true;
    }
    return false;
}

流程详解:

  1. tryRelease(arg)(子类实现):
    • 修改 state,如果 state=0,表示完全释放。
  2. unparkSuccessor(h)
    • 找到 head 后第一个未被取消的节点。
    • 调用 LockSupport.unpark(node.thread) 唤醒线程。

唤醒后的行为:

  • 被唤醒的线程回到 acquireQueued 的循环,再次尝试 tryAcquire
  • 如果成功,它将成为新的 head,并继续执行。

4. 公平锁 vs 非公平锁

4.1 非公平锁(默认)

特点:

  • 新线程可以“插队”,直接尝试 CAS 获取锁,无需排队。
  • 吞吐量更高,但可能导致“饥饿”。

实现 (NonfairSync):

final void lock() {
    if (compareAndSetState(0, 1))  // 先尝试插队
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);  // 失败再走正常流程
}

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);  // 不检查队列,直接 CAS
}

4.2 公平锁

特点:

  • 严格按照 FIFO 顺序获取锁,新线程必须排队。
  • 避免“饥饿”,但性能较低。

实现 (FairSync):

protected final boolean tryAcquire(int acquires) {
    if (!hasQueuedPredecessors() &&  // 检查是否有前驱节点
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
    return false;
}

对比总结:

特性 非公平锁 公平锁
获取策略 允许插队 严格 FIFO
性能 更高(减少线程切换) 较低(严格排队)
适用场景 高并发、锁竞争激烈 需要严格顺序的场景

5. 总结

  • AQS 的核心state + CLH 变体队列 + LockSupport 阻塞/唤醒。
  • 同步队列管理:通过 Node 节点、head/tail 指针实现线程排队。
  • 公平性控制:由 tryAcquire 是否检查队列决定。
  • 适用场景
    • 非公平锁:大多数高并发场景(如 ReentrantLock 默认)。
    • 公平锁:需要严格顺序的场景(如任务调度)。

AQS 是 Java 并发包的基石,理解它的实现有助于深入掌握 ReentrantLockSemaphoreCountDownLatch 等同步工具的工作原理。


网站公告

今日签到

点亮在社区的每一天
去签到