- 引言
- 一、为什么需要AQS?
- 二、AQS核心思想:一套通用的并发框架
- 三、核心组件解析
- 四、两种模式:独占与共享
- 五、源码流程浅析
- 六、实战:用AQS实现一个简单的锁
- 七、总结与最佳实践
- 互动环节
引言
商业创意
在Java并发编程中,ReentrantLock、Semaphore、CountDownLatch等强大的同步工具类为我们解决了各种各样的线程协作难题。你是否曾好奇,这些功能各异的工具类,其底层是如何实现的?
答案就藏在
java.util.concurrent.locks.AbstractQueuedSynchronizer(AQS)这个看似晦涩的抽象类中。AQS是整个JUC包同步器的基石和灵魂,它用一个优雅的框架,封装了构建锁和同步器的核心细节。理解AQS,就如同获得了打开Java并发世界宝库的钥匙,让你能从“使用者”进阶为“理解者”和“创造者”。
一、为什么需要AQS?
在AQS出现之前,如果要实现一个自定义的锁或同步器,开发者需要直面复杂的线程排队、阻塞、唤醒等底层操作。这些操作不仅容易出错,而且难以优化。
AQS的设计目标:提供一个通用的、模板化的框架,让开发者可以专注于实现同步状态的获取与释放逻辑(即“什么是同步条件”),而将复杂的线程排队、等待、唤醒等机制(即“如何管理排队线程”)交给AQS底层统一实现。
简单来说:AQS负责管理“排队”,你(同步器的实现者)只负责定义“什么时候放人进去”。
二、AQS核心思想:一套通用的并发框架
AQS的核心思想可以用一个经典的银行办事大厅的比喻来理解:
- 状态(State):好比大厅里的空闲柜台数量。int state是AQS的一个volatile变量,是同步状态的核心。不同的同步器对state的含义有不同的解释:
- 对于ReentrantLock,state表示锁被重入的次数(0表示空闲,>0表示被占用)。
- 对于Semaphore,state表示剩余的许可证数量。
- 对于CountDownLatch,state表示还需要倒计数的数量。
- CLH队列:好比大厅里的排队等候区。AQS内部维护了一个FIFO的双向队列(一个变种的CLH队列),所有暂时无法获取到同步状态的线程都会被封装成Node节点,加入到这个队列中排队等待。
- 模板方法模式:AQS定义了顶级流程骨架(如acquire获取资源、release释放资源),但将一些关键步骤(如tryAcquire尝试获取资源)设计为protected方法,交由子类去实现。这就是“你定规则,我管排队” 的协作方式。
三、核心组件解析
1. 同步状态:state
这是一个volatile int类型的变量,是所有操作的核心。AQS提供了三种原子方法来操作它:
- getState(): 获取当前状态。
- setState(int newState): 设置状态。
- compareAndSetState(int expect, int update): 使用CAS操作原子性地更新状态,保证线程安全。
2. 节点(Node)与同步队列
无法立即获取锁的线程会被包装成一个Node节点。每个Node包含了:
- 代表的线程(Thread thread)
- 等待状态(int waitStatus),如CANCELLED(已取消)、SIGNAL(需要唤醒后继节点)等。
- 前驱指针(Node prev)和后继指针(Node next)
这些Node节点共同组成了一个双向的FIFO队列,即同步队列。AQS通过这个队列来管理所有等待线程的排队和唤醒。
四、两种模式:独占与共享
AQS支持两种工作模式,这决定了资源是如何被线程获取的。
1. 独占模式(Exclusive)
一次只有一个线程能成功获取资源,如ReentrantLock。
- 核心方法:
- acquire(int arg): 获取资源(模板方法,不可中断)。
- release(int arg): 释放资源(模板方法)。
- tryAcquire(int arg): 需要子类实现。尝试获取资源,成功返回true,失败返回false。
- tryRelease(int arg): 需要子类实现。尝试释放资源。
2. 共享模式(Shared)
多个线程可以同时成功获取资源,如Semaphore、CountDownLatch。
- 核心方法:
- acquireShared(int arg): 获取资源。
- releaseShared(int arg): 释放资源。
- tryAcquireShared(int arg): 需要子类实现。尝试获取资源。返回负数为失败;0表示成功,但后续共享获取可能不会成功;正数表示成功,且后续共享获取可能成功。
- tryReleaseShared(int arg): 需要子类实现。尝试释放资源。
五、源码流程浅析
以独占模式的acquire方法为例,看看AQS是如何工作的:
// AQS中的模板方法 public final void acquire(int arg) { if (!tryAcquire(arg) && // 步骤1:子类尝试获取一次资源 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 步骤2&3:获取失败,则加入队列并等待 selfInterrupt(); // 如果是中断模式,则补上中断标记 }
- tryAcquire(arg):首先调用子类实现的tryAcquire方法尝试获取一次锁。如果成功,整个流程结束。
- addWaiter(Node.EXCLUSIVE):如果上一步尝试失败,则将当前线程包装成一个独占模式的Node节点,并通过CAS操作快速添加到同步队列的尾部。
- acquireQueued(node, arg):这是核心中的核心。让已经入队的节点,以“自旋(循环尝试)”的方式不断尝试获取资源:
- 检查自己的前驱节点是不是头节点(head)。如果是,说明自己是排队的第一个,则再次调用tryAcquire尝试获取资源。
- 如果获取成功,将自己设为新的头节点,并脱离队列。
- 如果前驱不是头节点,或者尝试再次失败,则可能会挂起(park) 当前线程,等待被前驱节点唤醒。
- 被唤醒后,继续循环检查自己是否是头节点的后继,并尝试获取资源。
release流程相对简单:
- 调用子类实现的tryRelease(arg)尝试释放资源。
- 如果释放成功(state变为0),则唤醒同步队列中头节点的后继节点(即下一个等待的线程)。
六、实战:用AQS实现一个简单的锁
理解了原理,让我们动手实现一个最简单的(非重入)互斥锁Mutex。
import java.util.concurrent.locks.AbstractQueuedSynchronizer; import java.util.concurrent.locks.Lock; /** * 一个简单的不可重入互斥锁 */ public class Mutex implements Lock { // 静态内部类,继承AQS,实现具体的同步逻辑 private static class Sync extends AbstractQueuedSynchronizer { // 是否处于占用状态(state为1表示占用,0表示空闲) @Override protected boolean isHeldExclusively() { return getState() == 1; } // 尝试获取锁(AQS模板方法acquire会调用此方法) @Override protected boolean tryAcquire(int acquires) { // 断言: acquires 必须为 1 assert acquires == 1; // 使用CAS操作,尝试将state从0改为1 if (compareAndSetState(0, 1)) { // 成功!设置当前线程为独占所有者 setExclusiveOwnerThread(Thread.currentThread()); return true; } // CAS失败,获取锁失败 return false; } // 尝试释放锁(AQS模板方法release会调用此方法) @Override protected boolean tryRelease(int releases) { // 断言: releases 必须为 1 assert releases == 1; // 如果状态已经是0,说明锁已经是空闲的,释放操作异常 if (getState() == 0) { throw new IllegalMonitorStateException(); } // 释放锁,将独占所有者清空 setExclusiveOwnerThread(null); // 注意:state的volatile写放在最后,保证之前的修改对获取锁的线程可见 setState(0); return true; } } // 将具体工作代理给Sync对象 private final Sync sync = new Sync(); @Override public void lock() { sync.acquire(1); // 调用AQS的模板方法,AQS会去调用我们重写的tryAcquire } @Override public void unlock() { sync.release(1); // 调用AQS的模板方法,AQS会去调用我们重写的tryRelease } // 其他Lock接口方法(略实现) @Override public void lockInterruptibly() throws InterruptedException { sync.acquireInterruptibly(1); } @Override public boolean tryLock() { return sync.tryAcquire(1); } @Override public boolean tryLock(long time, java.util.concurrent.TimeUnit unit) throws InterruptedException { return sync.tryAcquireNanos(1, unit.toNanos(time)); } @Override public java.util.concurrent.Condition newCondition() { // 简单实现,不支持Condition throw new UnsupportedOperationException(); } }
使用这个自定义的Mutex锁:
public class MutexExample { private static final Mutex lock = new Mutex(); private static int count = 0; public static void main(String[] args) throws InterruptedException { Runnable task = () -> { lock.lock(); // 获取我们自定义的锁 try { for (int i = 0; i < 10000; i++) { count++; } } finally { lock.unlock(); // 释放锁 } }; Thread t1 = new Thread(task); Thread t2 = new Thread(task); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println("Final count: " + count); // 正确输出 20000 } }
通过这个简单的例子,你可以清晰地看到AQS是如何工作的:我们只定义了“获取锁的条件是CAS(0,1)成功”,而线程排队、阻塞、唤醒等复杂机制全部由AQS的acquire和release模板方法帮我们完成了。
七、总结与最佳实践
- AQS的地位:AQS是JUC同步组件的基石,ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock等都是基于它构建的。
- 核心机制:通过一个volatile int state表示资源状态,一个FIFO队列管理排队线程, combined with CAS操作和模板方法模式,构建了一套高效、通用的同步框架。
- 设计模式:AQS是模板方法模式的经典应用。父类定义算法骨架,子类实现可变细节。
- 最佳实践:
- 作为使用者:理解AQS能让你更深刻地理解各种JUC同步工具类的原理和特性,从而更好地使用它们。
- 作为开发者:除非有极其特殊的同步需求,否则应优先直接使用JUC包提供的现成同步器,而不是自己基于AQS造轮子。它们已经经过千锤百炼,是高效且稳定的。
- 若要自定义:如果需要实现一个全新的、现有同步器无法满足需求的同步原语,继承AQS并重写tryAcquire、tryRelease等方法是最佳选择。
AQS是Java并发大师Doug Lea的作品,其设计之精妙,堪称艺术品。理解它,不仅是为了应对面试,更是为了提升我们对并发编程本质的认识,培养设计复杂系统的架构能力。