引言
在Java并发编程领域,java.util.concurrent包(JUC)提供了丰富且强大的并发工具。而这些工具的基石,便是AbstractQueuedSynchronizer(AQS),抽象队列同步器。AQS不仅是ReentrantLock、Semaphore、CountDownLatch等并发组件的底层实现,更是理解Java并发机制的关键。本文将探讨AQS的设计思想、核心原理、内部数据结构以及其在独占模式和共享模式下的应用。
1. AQS概述
AQS,全称AbstractQueuedSynchronizer,意为抽象队列同步器。它是一个用于构建锁和同步器的框架,提供了一种实现依赖于先进先出(FIFO)等待队列的阻塞锁和相关同步器的机制。AQS的核心思想是,如果被请求的共享资源空闲,那么就将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一套线程阻塞等待以及被唤醒时锁分配的机制,AQS就是解决了这个问题。
1.1 AQS的特性
- 阻塞和唤醒机制:当线程尝试获取资源失败时,AQS会将其封装成一个
Node节点加入到同步队列中,并进行阻塞。当资源被释放时,AQS会唤醒同步队列中的下一个等待线程。 - 状态管理:AQS内部使用一个
volatile修饰的int类型的成员变量state来表示同步状态。这个state变量是AQS实现同步机制的核心,通过对它的修改来表示资源的占用和释放。例如,在ReentrantLock中,state表示锁的重入次数;在Semaphore中,state表示可用的许可数量。 - FIFO同步队列:AQS维护了一个FIFO的双向链表作为同步队列,用于管理等待获取资源的线程。当线程获取资源失败时,会被包装成一个
Node节点并加入到这个队列的尾部。当资源被释放时,队列头部的线程会被唤醒并尝试获取资源。 - 模板方法模式:AQS本身是一个抽象类,它定义了同步器实现的基本骨架,但具体的资源获取和释放逻辑则由子类通过重写其模板方法来实现。这使得AQS具有很高的可扩展性,开发者可以基于AQS轻松实现各种自定义同步器。
1.2 AQS的地位
AQS是JUC包中许多核心并发组件的基石,例如:
ReentrantLock:可重入的独占锁,支持公平锁和非公平锁。Semaphore:信号量,用于控制同时访问特定资源的线程数量。CountDownLatch:倒计时门闩,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。ReentrantReadWriteLock:可重入的读写锁,允许多个读线程同时访问,但只允许一个写线程访问。ThreadPoolExecutor中的Worker线程的启动和关闭也间接依赖于AQS。
理解AQS的原理,对于深入理解这些并发工具的内部工作机制至关重要。
2. AQS核心原理
AQS的核心原理围绕着三个关键要素展开:同步状态(state)、FIFO同步队列(CLH队列)以及对同步状态的原子性操作(CAS)。
2.1 同步状态(state)
AQS使用一个volatile int state成员变量来表示同步状态。这个变量是AQS实现所有同步逻辑的基础。不同的同步器对state的含义有不同的解释:
- 独占锁(如
ReentrantLock):state为0表示锁未被占用,为1表示锁已被占用。对于可重入锁,state的值可以大于1,表示当前线程获取锁的次数。 - 共享锁(如
Semaphore):state表示当前可用的资源数量。当state为0时,表示资源已耗尽。
state变量被声明为volatile,确保了其在多线程环境下的可见性。AQS提供了三个protected方法来访问和修改state:
getState():获取当前同步状态的值。setState(int newState):设置当前同步状态的值。compareAndSetState(int expect, int update):使用CAS(Compare-And-Swap)操作原子性地更新同步状态。这是AQS实现无锁并发的关键。
2.2 FIFO同步队列(CLH队列)
AQS内部维护了一个双向链表,即FIFO(First-In, First-Out)同步队列,也常被称为CLH(Craig, Landin, and Hagersten)队列的变体。这个队列用于管理所有等待获取资源的线程。当一个线程尝试获取资源失败时,它会被封装成一个Node节点,并加入到同步队列的尾部。每个Node节点除了包含线程本身,还包含线程的等待状态(waitStatus)以及指向前驱和后继节点的引用。
Node的结构:
Node是AQS的静态内部类,它定义了等待队列中节点的结构,主要包含以下字段:
thread:当前节点关联的线程。prev:指向前驱节点。next:指向后继节点。waitStatus:表示当前节点的等待状态,有以下几种:CANCELLED (1):表示当前节点已取消。当线程等待超时或被中断时,会进入此状态。SIGNAL (-1):表示后继节点需要被唤醒。当前节点释放锁或取消时,会唤醒后继节点。CONDITION (-2):表示节点在条件队列中等待。当调用Condition.await()方法时,线程会进入此状态。PROPAGATE (-3):表示共享模式下,当前节点不仅会唤醒后继节点,还会传播唤醒操作。主要用于releaseShared方法。0:初始状态。
同步队列的工作流程:
- 入队:当线程尝试获取资源失败时,AQS会创建一个
Node节点,将当前线程封装进去,并通过CAS操作将其添加到同步队列的尾部。如果添加失败(例如,在并发环境下其他线程同时尝试入队),会进行自旋重试。 - 阻塞:线程入队后,会检查其前驱节点的
waitStatus。如果前驱节点的状态是SIGNAL,则当前线程会安全地挂起(park),等待被唤醒。如果不是SIGNAL,则会尝试将前驱节点的状态设置为SIGNAL,并再次检查。 - 出队与唤醒:当持有资源的线程释放资源时,它会唤醒同步队列中的头节点(通常是队列中等待时间最长的线程)。被唤醒的线程会再次尝试获取资源。如果获取成功,它将成为新的头节点,并从队列中移除。
2.3 CAS(Compare-And-Swap)操作
CAS是一种乐观锁机制,它包含三个操作数:内存位置(V)、预期原值(A)和新值(B)。如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置的值更新为新值B。否则,处理器不做任何操作。AQS大量使用CAS操作来保证对state变量和同步队列的原子性修改,从而避免了传统锁带来的开销。
例如,在获取独占锁时,AQS会尝试使用CAS将state从0设置为1。如果成功,则表示获取锁成功;如果失败,则说明有其他线程已经获取了锁,当前线程需要进入等待队列。
// 示例:AQS中尝试获取独占锁的核心逻辑(简化版)
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 尝试快速入队
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
// 慢速入队(自旋)
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 示例:AQS中尝试释放独占锁的核心逻辑(简化版)
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
上述代码片段展示了AQS中acquire和release方法的核心逻辑,其中tryAcquire和tryRelease是需要子类实现的模板方法,它们定义了具体的资源获取和释放策略。addWaiter和enq方法负责将线程添加到同步队列,unparkSuccessor负责唤醒后继线程。
3. AQS独占模式
独占模式(Exclusive Mode)是指在同一时刻,只有一个线程能够成功获取同步状态。其他尝试获取同步状态的线程都将被阻塞,并进入同步队列等待。典型的独占模式同步器有ReentrantLock和ReentrantReadWriteLock的写锁。
3.1 独占模式的获取(acquire)
当线程调用acquire(int arg)方法尝试获取独占锁时,其内部逻辑大致如下:
- 尝试获取同步状态:首先调用子类实现的
tryAcquire(int arg)方法。这个方法是AQS的模板方法,由具体的同步器实现,用于尝试以独占方式获取同步状态。例如,在ReentrantLock中,tryAcquire会尝试通过CAS操作将state从0设置为1,或者在重入时增加state的值。 - 获取成功:如果
tryAcquire返回true,表示当前线程成功获取了同步状态,acquire方法直接返回。 - 获取失败:如果
tryAcquire返回false,表示同步状态已被其他线程占用。此时,当前线程会被封装成一个Node节点,并加入到同步队列的尾部(通过addWaiter方法)。 - 阻塞等待:线程进入同步队列后,会通过
acquireQueued方法进行自旋,并检查其前驱节点的waitStatus。如果前驱节点是SIGNAL状态,则当前线程会安全地挂起(park),等待被唤醒。这种机制避免了不必要的CPU消耗,只有当前驱节点释放锁时,才会被唤醒。 - 中断响应:在阻塞等待过程中,如果线程被中断,
acquire方法会根据实现(acquire或acquireInterruptibly)选择是否响应中断。如果响应中断,则会抛出InterruptedException。
3.2 独占模式的释放(release)
当线程调用release(int arg)方法释放独占锁时,其内部逻辑大致如下:
- 尝试释放同步状态:首先调用子类实现的
tryRelease(int arg)方法。这个方法由具体的同步器实现,用于尝试以独占方式释放同步状态。例如,在ReentrantLock中,tryRelease会尝试减少state的值,当state减到0时,表示锁完全释放。 - 释放成功:如果
tryRelease返回true,表示当前线程成功释放了同步状态。此时,AQS会检查同步队列的头节点。如果头节点存在且其waitStatus不是0(通常是SIGNAL),则会唤醒头节点中的线程(通过unparkSuccessor方法)。 - 释放失败:如果
tryRelease返回false,表示同步状态尚未完全释放(例如,在可重入锁中,state尚未减到0),release方法直接返回。
3.3 独占模式示例:ReentrantLock
ReentrantLock是Java中最常用的独占锁之一,其内部实现正是基于AQS。它支持可重入性,即同一个线程可以多次获取同一把锁,并且需要释放相同次数的锁才能真正释放。
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockExample {
private static ReentrantLock lock = new ReentrantLock();
private static int count = 0;
public static void increment() {
lock.lock(); // 获取锁
try {
for (int i = 0; i < 10000; i++) {
count++;
}
} finally {
lock.unlock(); // 释放锁
}
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> increment());
Thread t2 = new Thread(() -> increment());
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("Final count: " + count);
}
}
在上述示例中,lock.lock()方法会调用AQS的acquire方法,而lock.unlock()方法会调用AQS的release方法。ReentrantLock的内部类Sync(公平或非公平)实现了tryAcquire和tryRelease方法,从而实现了独占锁的逻辑。
4. AQS共享模式
共享模式(Shared Mode)是指在同一时刻,多个线程可以同时获取同步状态。当一个线程获取同步状态后,其他线程仍然可以尝试获取,只要同步状态允许。典型的共享模式同步器有Semaphore和CountDownLatch,以及ReentrantReadWriteLock的读锁。
4.1 共享模式的获取(acquireShared)
当线程调用acquireShared(int arg)方法尝试获取共享锁时,其内部逻辑大致如下:
- 尝试获取同步状态:首先调用子类实现的
tryAcquireShared(int arg)方法。这个方法是AQS的模板方法,由具体的同步器实现,用于尝试以共享方式获取同步状态。该方法返回一个int值,表示剩余的同步状态。如果返回负数,表示获取失败;如果返回0,表示获取成功但没有剩余同步状态;如果返回正数,表示获取成功且有剩余同步状态。 - 获取成功:如果
tryAcquireShared返回非负数,表示当前线程成功获取了同步状态。此时,AQS会进一步判断是否需要传播唤醒操作(setHeadAndPropagate),以唤醒同步队列中的其他等待线程,特别是那些也想以共享模式获取资源的线程。 - 获取失败:如果
tryAcquireShared返回负数,表示同步状态不足。此时,当前线程会被封装成一个Node节点,并加入到同步队列的尾部。 - 阻塞等待:线程进入同步队列后,会通过
doAcquireShared方法进行自旋,并检查其前驱节点的waitStatus。如果前驱节点是SIGNAL状态,则当前线程会安全地挂起(park),等待被唤醒。 - 中断响应:与独占模式类似,在阻塞等待过程中,如果线程被中断,
acquireShared方法会根据实现选择是否响应中断。
4.2 共享模式的释放(releaseShared)
当线程调用releaseShared(int arg)方法释放共享锁时,其内部逻辑大致如下:
- 尝试释放同步状态:首先调用子类实现的
tryReleaseShared(int arg)方法。这个方法由具体的同步器实现,用于尝试以共享方式释放同步状态。该方法返回true表示同步状态完全释放,可以唤醒后续等待线程;返回false表示同步状态尚未完全释放。 - 释放成功并传播唤醒:如果
tryReleaseShared返回true,表示同步状态已被释放。此时,AQS会检查同步队列的头节点。如果头节点存在且其waitStatus不是0,则会唤醒头节点中的线程(通过doReleaseShared方法)。与独占模式不同的是,共享模式下的释放可能会导致连锁唤醒,即一个线程释放资源后,可能会唤醒多个等待线程,这些线程又可能继续唤醒后续线程,直到没有可用的资源或者队列为空。
4.3 共享模式示例:Semaphore
Semaphore(信号量)是AQS共享模式的一个典型应用。它维护了一个许可集,线程可以通过acquire()方法获取许可,通过release()方法释放许可。如果许可数量不足,线程将被阻塞。
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
private static Semaphore semaphore = new Semaphore(3); // 允许3个线程同时访问
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可
System.out.println(Thread.currentThread().getName() + " 正在执行");
Thread.sleep(2000); // 模拟业务逻辑
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println(Thread.currentThread().getName() + " 执行完毕");
semaphore.release(); // 释放许可
}
}, "Thread-" + i).start();
}
}
}
在上述示例中,semaphore.acquire()方法会调用AQS的acquireShared方法,而semaphore.release()方法会调用AQS的releaseShared方法。Semaphore的内部类Sync实现了tryAcquireShared和tryReleaseShared方法,从而实现了信号量的逻辑。
5. 如何基于AQS实现自定义同步器
AQS之所以被称为“抽象队列同步器”,是因为它提供了一个灵活的框架,允许开发者通过继承AQS并实现其抽象方法来构建自己的同步器。这正是AQS的强大之处,它将同步器的通用逻辑(如队列管理、线程阻塞/唤醒)与具体同步逻辑(如资源获取/释放条件)分离。
实现自定义同步器通常需要重写以下一个或多个AQS的protected方法:
5.1 独占模式下需要重写的方法
protected boolean tryAcquire(int arg):尝试以独占模式获取同步状态。如果成功,返回true;否则返回false。实现时需要考虑同步状态的获取条件和CAS操作。protected boolean tryRelease(int arg):尝试以独占模式释放同步状态。如果成功,返回true;否则返回false。实现时需要考虑同步状态的释放条件。protected boolean isHeldExclusively():查询当前线程是否独占同步状态。通常用于调试和内部检查。
5.2 共享模式下需要重写的方法
protected int tryAcquireShared(int arg):尝试以共享模式获取同步状态。如果成功,返回一个非负数,表示剩余的同步状态;如果失败,返回一个负数。实现时需要考虑同步状态的获取条件和CAS操作。protected boolean tryReleaseShared(int arg):尝试以共享模式释放同步状态。如果成功,返回true,表示可以唤醒后续等待线程;否则返回false。实现时需要考虑同步状态的释放条件。
5.3 条件队列(ConditionObject)
AQS还提供了一个内部类ConditionObject,用于实现条件队列。条件队列与同步队列是相互独立的,它允许线程在满足特定条件时才被唤醒。每个ConditionObject实例都与一个AQS同步器关联,并且可以有多个条件队列。这使得开发者可以实现更复杂的同步逻辑,例如生产者-消费者模式中的等待/通知机制。
使用ConditionObject通常涉及以下步骤:
- 创建
Condition实例:通过AQS的newCondition()方法创建Condition实例。 - 等待条件:当线程需要等待某个条件时,调用
condition.await()方法。此时,线程会释放持有的锁,并进入条件队列等待。当条件满足时,其他线程会通过condition.signal()或condition.signalAll()方法唤醒等待线程。 - 通知条件:当某个条件满足时,调用
condition.signal()(唤醒一个等待线程)或condition.signalAll()(唤醒所有等待线程)方法。被唤醒的线程会重新尝试获取锁,并在获取成功后从await()方法返回。
5.4 自定义同步器示例:一个简单的独占锁
下面是一个基于AQS实现的一个简单独占锁的示例,它不具备可重入性,仅用于演示AQS的基本用法:
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
public class SimpleLock {
// 静态内部类,继承AQS
private static class Sync extends AbstractQueuedSynchronizer {
// 判断是否处于锁定状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取锁
@Override
protected boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) { // 尝试将state从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); // 将state设置为0
return true;
}
}
private final Sync sync = new Sync();
// 获取锁
public void lock() {
sync.acquire(1);
}
// 释放锁
public void unlock() {
sync.release(1);
}
// 判断是否被锁定
public boolean isLocked() {
return sync.isHeldExclusively();
}
public static void main(String[] args) throws InterruptedException {
SimpleLock lock = new SimpleLock();
Thread t1 = new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
}, "Thread-1");
Thread t2 = new Thread(() -> {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " acquired the lock.");
} finally {
lock.unlock();
System.out.println(Thread.currentThread().getName() + " released the lock.");
}
}, "Thread-2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
在这个示例中,SimpleLock通过内部类Sync继承了AQS,并重写了tryAcquire和tryRelease方法来定义锁的获取和释放逻辑。tryAcquire通过CAS操作尝试将state从0设置为1,表示获取锁;tryRelease则将state设置为0,表示释放锁。acquire和release方法则直接调用了AQS提供的模板方法。
6. 总结与展望
AQS作为Java并发包的核心组件,其设计精妙且功能强大。它通过volatile int state、FIFO同步队列和CAS操作,为各种高级同步器提供了统一且高效的实现框架。无论是独占模式下的ReentrantLock,还是共享模式下的Semaphore,都离不开AQS的底层支持。
深入理解AQS,不仅能够帮助我们更好地使用JUC包中的并发工具,还能够为我们设计和实现高性能、高可用的自定义同步器提供坚实的基础。未来,随着Java平台和并发编程技术的发展,AQS及其衍生出的同步器将继续在构建健壮、高效的并发应用中发挥核心作用。
深入解析Java AQS并发核心
7847

被折叠的 条评论
为什么被折叠?



