Java 的 AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 java.util.concurrent.locks
包中用于构建锁和同步器的基础框架。AQS 提供了一个实现线程 同步状态管理 和 线程阻塞队列 的基础骨架,Java 中许多并发工具类如 ReentrantLock
、Semaphore
、CountDownLatch
都是基于 AQS 构建的。
下面我们将从 AQS 的基本结构、原理、关键方法 和 工作流程 来深入讲解,最后会举例说明如何通过 AQS 构建自定义同步器。
1. AQS 的基本结构
AQS 通过一个 FIFO(先进先出)等待队列 来管理竞争锁或资源的线程,同时通过一个整数表示共享资源的 同步状态。在 AQS 中,线程争夺资源的过程中会处于 独占模式(Exclusive)或 共享模式(Shared),这两种模式的区别在于:
- 独占模式:只有一个线程可以访问资源(如
ReentrantLock
)。 - 共享模式:多个线程可以同时访问资源(如
Semaphore
、CountDownLatch
)。
AQS 的核心数据结构有两个:
- 同步状态(state):一个
int
类型的变量,表示当前锁或者资源的状态(例如:是否被占用,计数值等)。 - 等待队列(CLH队列):线程争夺资源时,会加入一个 等待队列,等待资源可用时被唤醒。
1.1 CLH 队列(FIFO队列)
AQS 使用一种变种的 CLH队列(Craig, Landin, and Hagersten)来管理线程,CLH 是一种 双向链表结构,每个节点代表一个等待的线程。
每个节点包含:
prev
:指向前一个节点(前驱节点)。next
:指向后一个节点(后继节点)。thread
:当前等待的线程。waitStatus
:节点的等待状态,如SIGNAL
、CANCELLED
、CONDITION
等。
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):
- 共享模式允许多个线程同时访问资源。比如
Semaphore
和CountDownLatch
。 - 共享模式主要通过以下方法实现:
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()
)为例:
- 尝试获取锁:首先通过
tryAcquire()
尝试获取资源。如果获取成功,线程继续执行。 - 加入等待队列:如果获取失败,线程会被加入 CLH 队列,进入等待状态。
- 自旋等待:当线程位于队列的头节点时,它会不断自旋尝试重新获取资源(在某个线程释放资源后)。
- 获取资源:当前线程成功获取资源后,从队列中移除并继续执行。
释放锁(release()
):
- 释放资源:通过
tryRelease()
来释放资源,并更新同步状态。 - 唤醒后继节点:如果有线程在等待队列中,则唤醒下一个节点,让其尝试获取资源。
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 并实现
tryAcquire
、tryRelease
等方法,可以轻松构建自定义的同步器。 - AQS 使用 CAS 原子操作 和 FIFO 等待队列 来确保线程同步的安全性。
通过理解 AQS 的工作机制和原理,可以更深入地掌握 Java 中的并发编程,并为构建复杂的并发同步器提供基础。