java 的 AQS

Java 的 AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 java.util.concurrent.locks 包中用于构建锁和同步器的基础框架。AQS 提供了一个实现线程 同步状态管理线程阻塞队列 的基础骨架,Java 中许多并发工具类如 ReentrantLockSemaphoreCountDownLatch 都是基于 AQS 构建的。

下面我们将从 AQS 的基本结构原理关键方法工作流程 来深入讲解,最后会举例说明如何通过 AQS 构建自定义同步器。

1. AQS 的基本结构

AQS 通过一个 FIFO(先进先出)等待队列 来管理竞争锁或资源的线程,同时通过一个整数表示共享资源的 同步状态。在 AQS 中,线程争夺资源的过程中会处于 独占模式(Exclusive)或 共享模式(Shared),这两种模式的区别在于:

  • 独占模式:只有一个线程可以访问资源(如 ReentrantLock)。
  • 共享模式:多个线程可以同时访问资源(如 SemaphoreCountDownLatch)。

AQS 的核心数据结构有两个:

  • 同步状态(state):一个 int 类型的变量,表示当前锁或者资源的状态(例如:是否被占用,计数值等)。
  • 等待队列(CLH队列):线程争夺资源时,会加入一个 等待队列,等待资源可用时被唤醒。

1.1 CLH 队列(FIFO队列)

AQS 使用一种变种的 CLH队列(Craig, Landin, and Hagersten)来管理线程,CLH 是一种 双向链表结构,每个节点代表一个等待的线程。

每个节点包含:

  • prev:指向前一个节点(前驱节点)。
  • next:指向后一个节点(后继节点)。
  • thread:当前等待的线程。
  • waitStatus:节点的等待状态,如 SIGNALCANCELLEDCONDITION 等。

2. AQS 的核心机制和原理

AQS 的核心是通过对 同步状态 的管理以及 等待队列 来实现线程的同步。

2.1 同步状态管理

AQS 提供了几个重要的原子操作来修改 同步状态(state)。这些操作依赖于底层的 CAS(Compare-And-Swap) 原子操作,来确保多线程下的安全性:

  • getState():获取当前同步状态。
  • setState(int newState):设置同步状态。
  • compareAndSetState(int expect, int update):通过 CAS 操作修改同步状态,只有当当前状态与预期的 expect 值相等时,才会更新为 update 值。

在独占模式下,同步状态的典型使用场景是 0 表示锁空闲,1 表示锁已被占用,而共享模式下,同步状态通常是一个 计数值

2.2 独占模式与共享模式

  • 独占模式(Exclusive)
    • 独占模式下,一个线程独占资源,其他线程只能等待,直到资源释放。例如,ReentrantLock 是典型的独占模式实现。
    • 独占模式主要通过以下方法实现:
      • tryAcquire(int arg):尝试获取资源,成功返回 true,否则返回 false
      • tryRelease(int arg):尝试释放资源。
  • 共享模式(Shared)
    • 共享模式允许多个线程同时访问资源。比如 SemaphoreCountDownLatch
    • 共享模式主要通过以下方法实现:
      • tryAcquireShared(int arg):尝试共享获取资源,返回大于等于 0 表示获取成功,返回负数表示获取失败。
      • tryReleaseShared(int arg):尝试共享释放资源。

3. AQS 的关键方法

AQS 的核心方法通常由子类实现,AQS 主要提供的操作有:

  • acquire(int arg):以独占模式获取资源,若获取失败则进入等待队列。
  • release(int arg):以独占模式释放资源,唤醒后继节点。
  • acquireShared(int arg):以共享模式获取资源,若获取失败则进入等待队列。
  • releaseShared(int arg):以共享模式释放资源,唤醒后继节点。

这些方法通过 CAS 操作来修改同步状态,并通过维护 CLH 队列来管理线程的调度与唤醒。

4. AQS 的工作流程

4.1 独占模式的工作流程

以独占模式获取锁(acquire())为例:

  1. 尝试获取锁:首先通过 tryAcquire() 尝试获取资源。如果获取成功,线程继续执行。
  2. 加入等待队列:如果获取失败,线程会被加入 CLH 队列,进入等待状态。
  3. 自旋等待:当线程位于队列的头节点时,它会不断自旋尝试重新获取资源(在某个线程释放资源后)。
  4. 获取资源:当前线程成功获取资源后,从队列中移除并继续执行。

释放锁(release()):

  1. 释放资源:通过 tryRelease() 来释放资源,并更新同步状态。
  2. 唤醒后继节点:如果有线程在等待队列中,则唤醒下一个节点,让其尝试获取资源。

4.2 共享模式的工作流程

共享模式的工作流程类似,但多个线程可以同时访问资源。

5. 如何通过 AQS 实现自定义同步器

要创建一个自定义的同步器,通常需要继承 AQS 并实现以下方法:

  • tryAcquire(int arg):实现资源的独占获取逻辑。
  • tryRelease(int arg):实现资源的独占释放逻辑。
  • tryAcquireShared(int arg):实现资源的共享获取逻辑(如果需要)。
  • tryReleaseShared(int arg):实现资源的共享释放逻辑(如果需要)。

示例:自定义二元锁(即一次只允许一个线程获取锁)

javaCopy codeclass MyLock extends AbstractQueuedSynchronizer {
    // 独占获取锁
    @Override
    protected boolean tryAcquire(int acquires) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }

    // 独占释放锁
    @Override
    protected boolean tryRelease(int releases) {
        if (getState() == 0) {
            throw new IllegalMonitorStateException();
        }
        setExclusiveOwnerThread(null);
        setState(0);
        return true;
    }

    // 加锁
    public void lock() {
        acquire(1);
    }

    // 解锁
    public void unlock() {
        release(1);
    }
}

6. AQS 线程安全的保证

AQS 通过以下机制来确保线程安全:

  • CAS 原子操作:通过 CAS 操作确保同步状态的更新是原子的,避免了多线程竞争。
  • FIFO 队列:AQS 的等待队列是一个 FIFO 队列,确保线程按照先后顺序获取资源。

7. 总结

  • AQS 是 Java 并发框架的核心组件,它通过管理同步状态和等待队列来实现多线程同步。
  • AQS 提供了两种模式:独占模式共享模式,分别用于实现独占访问和共享访问。
  • 通过继承 AQS 并实现 tryAcquiretryRelease 等方法,可以轻松构建自定义的同步器。
  • AQS 使用 CAS 原子操作FIFO 等待队列 来确保线程同步的安全性。

通过理解 AQS 的工作机制和原理,可以更深入地掌握 Java 中的并发编程,并为构建复杂的并发同步器提供基础。

0 0 投票数
Article Rating
订阅评论
提醒
guest
0 评论
最旧
最新 最多投票
内联反馈
查看所有评论
0
希望看到您的想法,请您发表评论x