一、引言
在 Java 并发编程的浩瀚星空中,AbstractQueuedSynchronizer(AQS)无疑是最为闪耀的恒星之一🌟。它作为 Java 并发包中众多同步器的基石,为 ReentrantLock、Semaphore、CountDownLatch 等工具提供了坚实的底层支撑,掌控着线程同步与资源共享的生杀大权。无论是构建复杂的多线程应用,还是深入理解 Java 并发的精妙之处,AQS 都是我们必须要攻克的关键堡垒。今天,就让我们一同揭开 AQS 神秘的面纱,探寻其背后的奥秘。
二、AQS 出现的原因
(一)线程协作工具类的困境
在 Java 的并发编程世界里,ReentrantLock、Semaphore 等线程协作类就像是一个个精密的齿轮⚙️,共同驱动着多线程程序高效运转。当我们深入探究它们的内部机制时,会发现一个有趣的共性 —— 它们都像是一个个 “阀门”,控制着线程对共享资源的访问。
以 ReentrantLock 为例,它如同一个 exclusive(独占)的阀门,同一时刻只允许一个线程持有锁,独占资源。当线程 A 获取到锁时,就相当于阀门关闭,其他线程只能在门外等待。直到线程 A 释放锁,阀门打开,其他线程才有机会竞争获取锁,进入临界区访问共享资源。这种机制确保了共享资源在同一时间只会被一个线程修改,避免了数据冲突和不一致的问题。
再看 Semaphore,它像是一个带有计数器的阀门,我们可以设定许可证的数量,比如设置为 3,这就意味着同时最多允许 3 个线程通过阀门访问资源。线程获取许可证的过程就像是向阀门申请通行许可,当许可证数量大于 0 时,线程可以顺利获取并进入,同时许可证数量减 1;一旦许可证耗尽,后续线程就只能阻塞等待,直到有线程归还许可证,阀门再次开放。
然而,这些看似功能各异的线程协作类,在实现过程中却面临着诸多难题。它们都需要精准地控制线程的阻塞与唤醒时机,管理共享资源的状态,并且要确保在高并发场景下这些操作的原子性和线程安全性。想象一下,每个工具类都要自己去实现一套复杂的线程调度逻辑:如何判断线程是否应该阻塞、如何将阻塞的线程安全地放入等待队列、又如何在条件满足时准确地唤醒等待的线程。这不仅需要对底层的线程同步机制有深入的理解,还涉及到大量繁琐且容易出错的代码实现。
(二)AQS 的解决方案
AbstractQueuedSynchronizer(AQS)应运而生,它宛如一位幕后英雄,将这些线程协作类的共性需求进行了高度抽象和封装。AQS 就像是为这些 “齿轮” 提供动力的核心引擎,承担了最为复杂、最易出错的线程调度细节。
它通过一个核心的同步状态变量(通常是一个 volatile 修饰的 int 型变量 state)来统一管理资源的状态。对于 ReentrantLock 来说,state 可以用来表示锁的持有情况,0 表示未被占用,大于 0 表示被某个线程持有,并且可以通过对 state 的操作实现锁的重入计数;对于 Semaphore,state 则代表着许可证的剩余数量,线程获取许可证时对 state 进行减操作,归还许可证时进行加操作。
同时,AQS 构建了一个先进先出(FIFO)的等待队列,用于存放那些未能获取到资源而被阻塞的线程。这个队列就像是一个井然有序的候车大厅,线程们按照先来后到的顺序排队等待。当资源可用时,AQS 会依据严格的规则从队列头部唤醒线程,确保公平性与高效性。
有了 AQS,像 ReentrantLock、Semaphore 等上层的线程协作工具类就如同被解放了双手,它们无需再深陷于复杂的线程调度泥潭,只需专注于自身独特的业务逻辑即可。例如,ReentrantLock 只需关注如何实现锁的公平性与非公平性获取策略,Semaphore 只需聚焦于许可证的分配规则。这大大降低了开发线程同步工具的难度,提高了代码的复用性与可维护性,使得 Java 并发编程变得更加得心应手。
三、AQS 的使用方式
(一)继承与方法重写
AQS 的使用宛如一场精心编排的舞蹈,第一步便是子类优雅地继承这个抽象大师🧑🎨。就像不同风格的舞者在同一舞台上展现独特风采,子类依据自身所需的同步特性,精准重写特定的方法。
对于独占模式而言,若想打造一把专属的锁🔒,子类就必须精心重写 tryAcquire 与 tryRelease 方法。以 tryAcquire 为例,它承载着线程获取独占资源的渴望,内部逻辑需通过 compareAndSetState 这个原子操作的魔法,巧妙地尝试将同步状态从空闲的 0 转换为忙碌的 1,一旦成功,便自豪地宣告当前线程成为资源的主宰,通过 setExclusiveOwnerThread 记录下这辉煌的时刻。而 tryRelease 则如同一场谢幕仪式,当线程完成使命,它负责将同步状态重置为 0,释放资源,让其他线程有机会登上舞台。
在共享模式的世界里,tryAcquireShared 和 tryReleaseShared 方法成为主角。tryAcquireShared 如同一场资源分配盛宴的邀请函,线程们纷纷前来申请共享资源,它根据当前资源的余量,返回不同的值:大于 0 意味着申请成功,资源充足;等于 0 表示虽获准入,但资源已达临界;小于 0 则宣告申请失败,只能等待下次机会。tryReleaseShared 则负责在共享资源使用完毕后,合理地调整资源计数,若条件允许,还会贴心地唤醒后续等待的线程,确保资源流转顺畅。
(二)借助模板方法实现自定义同步组件
有了精心雕琢的方法重写,下一步便是借助 AQS 提供的模板方法,将这些零散的舞步串联成一场华丽的演出。这里以一个自定义的 Mutex(互斥锁)同步组件为例,揭开这神秘面纱的一角。
public class Mutex implements Lock {
// 静态内部类,自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
@Override
protected boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 释放锁,将状态设置为0
@Override
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new IllegalMonitorStateException();
setExclusiveOwnerThread(null);
setState(0);
return true;
}
// 返回一个Condition,每个condition都包含了一个condition队列
Condition newCondition() {
return new ConditionObject();
}
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
@Override
public boolean isLocked() {
return sync.isHeldExclusively();
}
@Override
public boolean hasQueuedThreads() {
return sync.hasQueuedThreads();
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
}
在这段代码中,内部类 Sync 如同一位技艺精湛的工匠,继承了 AbstractQueuedSynchronizer,并精心重写了独占模式所需的核心方法,打造出专属的同步逻辑。而外部的 Mutex 类则像是一位优雅的指挥家,将所有的操作巧妙地代理到 Sync 上,通过调用 acquire、tryAcquire、release 等模板方法,实现了完整的锁功能。当线程调用 Mutex 的 lock 方法时,实则是委托 Sync 去争夺资源,若失败则会被安置在等待队列中,耐心等待唤醒的曙光;当线程执行 unlock 时,Sync 又负责释放资源,唤醒后继者,确保整个同步过程有条不紊地进行。
四、AQS 的实现原理
(一)核心数据结构
AQS 的内部宛如一座精密的时钟⏰,其核心数据结构相互协作,驱动着整个同步机制的运转。
首先,映入眼帘的是 state 变量,这个被 volatile 修饰的整型变量,犹如时钟的指针,精准地指示着同步状态。在不同的场景下,它有着不同的含义:对于独占锁(如 ReentrantLock),state 为 0 时,宛如一扇敞开的门,意味着锁未被占用,线程可以自由通行获取锁;当 state 为 1,则如同门已紧闭,代表锁被某个幸运的线程持有。不仅如此,ReentrantLock 的可重入特性更是让 state 大放异彩,它能记录线程重入的次数,每重入一次,state 就会相应递增,就像在门上叠加的一道道封印,确保同一线程在未完全释放锁之前,其他线程无法闯入。而对于共享锁(如 Semaphore),state 摇身一变,成为了资源的计数器,初始值便是许可证的数量,线程获取许可证时,state 减 1,归还许可证时,state 加 1,时刻把控着资源的余量。
接着,目光聚焦到 Node 节点,它如同时钟内部的齿轮,是构成 CLH 队列的基本单元。每个 Node 节点都像是一个小小的 “候车室”,精心保存着线程的信息。其中,thread 属性明确指向等待的线程,prev 和 next 指针则如同连接齿轮的链条,将各个节点串联成双向链表,使得线程在队列中的顺序一目了然。此外,waitStatus 变量宛如节点的 “信号灯”,有着不同的取值,代表着节点的不同状态:CANCELLED(值为 1)意味着线程已中途放弃,如同熄灭的灯火;SIGNAL(值为 -1)则是亮起的绿灯,表示后继线程需要被唤醒,接力前行;CONDITION(值为 -2)代表节点处于条件队列中,等待特定条件的触发;PROPAGATE(值为 -3)用于共享模式,当释放共享资源时,它像一阵东风,推动着释放操作向后传播,唤醒更多等待的节点。
最后,CLH 队列登场,它是基于 Node 节点构建的先进先出(FIFO)队列,恰似一条井然有序的流水线,严格管理着等待线程的顺序。队列的头部由 head 节点指引,尾部则由 tail 节点把关。当线程竞争资源失败时,就会被封装成 Node 节点,插入到队列的尾部,如同在流水线上依次排队等待处理。这种结构确保了线程按照先来后到的顺序获取资源,尽显公平公正,避免了混乱与冲突。
(二)获取锁流程
当线程怀揣着获取锁的渴望踏入这片领地时,一场精彩的 “资源争夺战” 拉开帷幕。
线程首先会调用 acquire 方法,这是获取锁的冲锋号。此方法内部逻辑严密,第一步便是触发 tryAcquire 尝试获取锁,这就像是线程伸手去抓那把开启资源之门的钥匙。若此时锁资源恰好空闲,仿佛门未上锁,tryAcquire 便能顺利通过 compareAndSetState 操作,将 state 标记为已占用,同时自豪地宣告自己成为资源的主宰,通过 setExclusiveOwnerThread 记录下这一辉煌时刻,整个过程如行云流水般顺畅,线程直接获取锁并开始执行任务。
然而,当锁已被其他线程牢牢掌控,如同紧闭的大门前已有人站岗,tryAcquire 尝试失败,线程并不会轻易气馁。它会迅速调整策略,调用 addWaiter 方法,将自己精心包装成一个 Node 节点,准备加入等待队列。这个过程就像是在拥挤的候车大厅中寻找自己的位置,线程先查看队列尾部 tail 节点是否存在,若 tail 不为空,便尝试通过 CAS 操作将自己节点设置为新的尾部,如同在人群中快速找到空位坐下;若 tail 为空或者 CAS 操作失败,意味着竞争激烈,此时 enq 方法登场,它通过自旋的方式,不断尝试初始化队列或插入节点,直至成功,确保线程最终能在队列中找到自己的一席之地。
成功入队后,线程并未停止脚步,紧接着进入 acquireQueued 方法,开启自旋等待之旅。在自旋过程中,线程会像一个执着的守望者,不断检查自己的前驱节点是否为头节点,因为只有前驱节点释放锁后,自己才有机会争夺资源。若前驱节点是头节点,线程会再次发起 tryAcquire 冲击,试图获取锁;若获取失败,继续自旋等待,期间若线程被中断,它会默默记录下来,待获取锁后再行处理。倘若自旋时间过长,超过一定阈值,为了避免浪费 CPU 资源,线程可能会选择阻塞,进入 park 状态,如同暂时休眠,等待被唤醒的那一声号角。
(三)释放锁流程
持有锁的线程在完成使命后,肩负起释放锁的重任,开启一场资源交接的接力赛。
线程首先调用 release 方法,这是释放锁的信号弹。方法内部,先由 tryRelease 方法登场,对于独占锁而言,它会小心翼翼地将 state 减 1,若减 1 后 state 变为 0,意味着锁已完全释放,如同放下手中的接力棒,此时会将独占线程标记清空,准备交接资源。若 tryRelease 成功执行,下一步便是唤醒后继节点,让等待的线程有机会接过资源的接力棒。
唤醒操作通过 unparkSuccessor 方法来实现,它如同吹响的起床号,唤醒沉睡的后继节点。首先,它会从队列头部开始查找,跳过那些已取消(waitStatus 为 CANCELLED)的节点,找到距离头节点最近且状态正常的后继节点,然后通过 Unsafe.unpark 操作唤醒该节点对应的线程,使其从阻塞状态恢复到就绪状态,重新加入资源竞争的赛道。
被唤醒的后继线程如同被激活的战士,迅速从自旋或阻塞状态中苏醒,再次尝试获取锁资源。它会重复获取锁流程中的步骤,先通过 tryAcquire 尝试获取锁,若成功则顺利接手资源,开始执行任务;若失败则继续在队列中自旋等待,如此循环往复,确保资源在不同线程间有序流转,整个同步机制得以稳定运行。
五、AQS 的使用场景
(一)ReentrantLock 中的应用
在 Java 并发编程的世界里,ReentrantLock 无疑是一把耀眼的利剑🗡️,而其强大威力的背后,离不开 AbstractQueuedSynchronizer(AQS)的默默支撑。
深入 ReentrantLock 的内部,我们会发现一个名为 Sync 的内部类,它宛如一座坚固的基石,继承自 AQS,承载着锁的核心逻辑。这个 Sync 类又进一步分化出两个得力干将:FairSync(公平锁)与 NonfairSync(非公平锁),它们以各自独特的策略,掌控着线程对锁资源的争夺。
当我们聚焦于非公平锁 NonfairSync 时,它的加锁过程充满了 “抢占先机” 的意味。想象一下,众多线程如潮水般涌来,争相获取锁资源。NonfairSync 不会让线程们乖乖排队,而是允许它们直接发起冲击,试图通过 CAS 操作将 AQS 的同步状态 state 从 0 瞬间切换为 1,一旦成功,便迅速将当前线程标记为锁的独占者,仿佛一位勇士在混乱的战场上率先抢占了高地,威风凛凛。若 CAS 操作不幸失败,意味着锁已被他人捷足先登,此时线程才会无奈地遵循 AQS 的规则,进入等待队列,等待被唤醒的机会。
而公平锁 FairSync 则截然不同,它秉持着 “先来后到” 的公平原则,如同一位公正的裁判,严格维持着秩序。当线程前来申请锁时,FairSync 会先仔细检查等待队列中是否已有前辈在等待。若队列空空如也,线程才有资格尝试通过 CAS 操作获取锁,一旦成功,便顺利成为锁的主人;若发现已有线程在耐心等待,即使当前线程心急如焚,也只能乖乖排在队列末尾,绝无插队的可能,充分展现了公平性的魅力。
无论是公平锁还是非公平锁,在解锁时,都会统一调用 AQS 的 release 方法。这个方法就像是一场交接仪式,先小心翼翼地将同步状态 state 减 1,若减 1 后 state 变为 0,表明锁已完全释放,此时便会唤醒等待队列中的后继线程,将锁资源顺利交接,确保线程间的协作有条不紊地进行,整个过程犹如一场精密编排的舞蹈,彰显着 AQS 与 ReentrantLock 协同的精妙之处。
(二)Semaphore 中的应用
Semaphore,这位 Java 并发编程中的 “流量指挥官”🚦,以其独特的方式调控着线程对资源的访问节奏,而这一切的幕后英雄依然是 AbstractQueuedSynchronizer(AQS)。
Semaphore 内部精心构建了一个基于 AQS 的同步机制,通过巧妙地设置同步状态 state,将其作为资源访问的许可证计数器🧮。初始化 Semaphore 时,我们可以指定许可证的数量,这个数量便会被精准地赋值给 state,仿佛给资源大门贴上了明确的 “准入标签”。
当线程怀揣着访问资源的渴望,调用 acquire 方法时,一场激烈的 “许可证争夺战” 就此打响。线程首先会尝试调用 AQS 的 tryAcquireShared 方法,这就像是向资源管理员申请许可证,若当前 state 大于 0,意味着尚有许可证余量,线程便能顺利获取,state 随之减 1,仿佛成功领取了一张珍贵的入场券,得以进入资源区域畅享资源;若 state 等于 0,残酷的现实摆在眼前 —— 资源已达访问上限,线程只能无奈地被封装成 Node 节点,加入 AQS 的等待队列,如同在拥挤的候场区等待入场机会,期间线程会进入阻塞状态,避免浪费宝贵的 CPU 资源。
而当线程完成资源访问,调用 release 方法时,宛如一位文明的离场者,主动归还许可证。此时会触发 AQS 的 tryReleaseShared 方法,将 state 加 1,若加 1 后 state 大于 0,说明有线程正在等待许可证,方法会贴心地唤醒等待队列中的后继线程,让它们有机会重新争夺许可证,继续前行,确保资源的流动顺畅无阻,整个过程在 AQS 的调度下,如同精密的齿轮咬合,高效且稳定。
(三)CountDownLatch 中的应用
在 Java 并发编程的舞台上,CountDownLatch 扮演着一位出色的 “协调大师”🎯,它能让主线程与子线程之间的协作变得井然有序,而这神奇效果的实现,离不开 AbstractQueuedSynchronizer(AQS)的强力加持。
CountDownLatch 内部同样运用了 AQS 的强大功能,以同步状态 state 作为核心计数器。创建 CountDownLatch 实例时,我们传入的计数值,便如同给一场接力赛设定的目标步数,被精准地设置为 state 的初始值,标志着这场线程协作的总任务量。
子线程们如同奋力奔跑的接力选手,在完成各自的任务后,会调用 countDown 方法,这一操作就像是选手们顺利交接棒,每调用一次,state 便减 1,意味着离终点又近了一步。而主线程则宛如那位在终点线前翘首以盼的教练,调用 await 方法,焦急地等待所有子线程完成任务。此时,主线程会尝试获取共享锁,若 state 尚未归零,说明仍有子线程在 “赛道” 上拼搏,主线程只能无奈地进入 AQS 的等待队列,进入阻塞状态,避免过早行动打乱节奏;一旦 state 归零,如同所有选手都成功冲过终点线,AQS 便会迅速唤醒主线程,让其继续后续的工作,确保整个任务流程的协调统一,展现出强大的线程同步能力。
六、总结
AbstractQueuedSynchronizer(AQS)作为 Java 并发编程的核心支柱,为多线程同步提供了强大且优雅的解决方案。通过深入剖析其出现的背景、精妙的使用方式、复杂的实现原理以及广泛的应用场景,我们得以一窥 Java 并发编程的深邃智慧。
AQS 的诞生源于对线程协作工具类共性难题的攻克,它以抽象封装的方式化解了线程调度、资源管理的复杂性,使得开发者能专注于业务逻辑的雕琢。在使用上,继承与模板方法模式相得益彰,为构建自定义同步组件开辟了便捷之路。其实现原理更是巧夺天工,核心数据结构与获取、释放锁流程紧密配合,确保了线程同步的高效与公平。
从 ReentrantLock 的独占风采、Semaphore 的流量调控,到 CountDownLatch 的协同指挥,AQS 的身影无处不在,为 Java 并发编程注入了强大动力。它不仅提升了程序性能,更保障了在多线程环境下数据的一致性与完整性。
对于每一位 Java 开发者而言,掌握 AQS 就等于握住了开启高效并发编程大门的钥匙。希望本文能成为您探索 AQS 之旅的得力向导,鼓励您在实践中不断深化对 AQS 的理解与运用,书写出更加健壮、高效的多线程代码,向着 Java 并发编程的巅峰奋勇攀登。