一、AQS 是啥?有啥用?
一、AQS 是啥?有啥用?
在 Java 并发编程的世界里,AbstractQueuedSynchronizer(简称 AQS)绝对是个 “狠角色”,它就像是隐藏在幕后的超级英雄,默默支撑着众多强大的并发工具。
AQS,从名字看就知道是个跟队列、同步相关的抽象神器。简单来说,它为 Java 里实现锁和其他同步组件打造了一个超实用的基础框架。有了它,开发者能轻松定制各种复杂的同步需求,像是 ReentrantLock(可重入锁)、Semaphore(信号量)、CountDownLatch(倒计时器)这些大名鼎鼎的并发工具,底层都离不开 AQS 的强力支撑。
和传统的 synchronized 关键字相比,AQS 的优势可不少。synchronized 用起来简单直接,编译器编译后会在同步块前后生成 monitorenter 和 monitorexit 字节码指令,靠对象头里的标记来控制锁的获取和释放。但它灵活性欠佳,像一些复杂的同步场景就有点应付不来。AQS 则不同,它把同步状态、线程排队这些底层逻辑都封装得妥妥当当,开发者可以根据需求定制同步规则,实现更精细的并发控制,而且在性能优化上也有更多的施展空间,能轻松应对高并发挑战。
为啥咱们要花心思去了解 AQS 呢?就算日常开发中,直接用现成的并发工具类就能搞定大部分问题,但学习 AQS 能让咱们深入理解 Java 并发编程的底层原理,掌握其精妙的设计思想,这对提升技术功底、攻克面试难题可太有帮助了。碰到棘手的并发问题,了解 AQS 就等于掌握了一把万能钥匙,能帮咱们快速分析问题、找到完美解决方案,让代码在多线程环境下跑得又快又稳。
二、核心原理剖析
2.1 状态 state:资源的掌控者
先来说说这个神秘的 state,它可是 AQS 里掌控资源命脉的关键角色,是个被 volatile 修饰的整型变量,用来标识资源的状态,就像交通信号灯一样,指挥着线程的行动。
在不同的工具类里,state 有着不同的含义。拿 ReentrantLock 来说,它表示锁的持有计数,初始值是 0,意味着锁空闲,没线程拿着。要是有线程成功获取锁,state 就变成 1;要是这锁可重入,同一线程再次获取,state 就会累加,释放一次锁,state 才减 1,直到变回 0,锁才彻底释放,别的线程才有机会。再看 CountDownLatch,state 充当计数器,初始设个正数,比如设置成 5,每个线程调用 countDown 方法,state 就减 1,等减到 0,就像发令枪响,那些因调用 await 方法而阻塞的线程就会被唤醒,撒腿狂奔。
AQS 为咱们提供了操作 state 的 “三件套” 方法:getState () 、setState (int) 、compareAndSetState (int, int)。getState () 就像个侦察兵,能安全地查看 state 当前值;setState () 是个直爽的指挥官,直接给 state 赋新值,不过它靠 volatile 保证线程安全;compareAndSetState () 则是个谨慎的决策者,用 CAS 操作(Compare and Swap,比较并交换)小心翼翼地更新 state,只有当前值和预期值一样,才会修改,保证原子性,底层是靠 Unsafe 类调用 CPU 指令来实现这神奇操作,这可是保证并发安全的大功臣。
2.2 FIFO 队列:线程的排队区
当多个线程嗷嗷叫着要抢资源,总得有个先来后到吧,这时候 AQS 里的 FIFO(先进先出)队列就登场啦。它就像个井然有序的排队通道,专门用来存放那些暂时没抢到资源、只能干瞪眼的线程,让它们规规矩矩排好队,等着轮到自己。
这个队列是个双向链表结构,由一个个 Node 节点串联起来。每个 Node 可不简单,除了包含代表线程的 thread 成员,还有几个关键的属性。waitStatus 是节点的 “心情指示灯”,取值有 CANCELLED(1,表示线程等得不耐烦,取消排队啦,可能是超时或者被中断了)、SIGNAL( -1,表示后面的节点正眼巴巴瞅着,等着当前节点释放资源后唤醒它呢)、CONDITION( -2,和条件队列有关,线程在等某个特定条件满足)、PROPAGATE( -3,在共享模式下用,涉及资源释放后唤醒后续节点的传播机制),初始是 0,表示没啥特殊情况,岁月静好。prev 和 next 分别指向节点的前一个和后一个邻居,方便线程们前后串联,维持队形。
当线程来抢资源,发现已经被占,就会被封装成 Node 节点,通过 addWaiter 方法加入队列尾巴。这里面可有个巧妙的 CAS 操作,先瞅一眼队列尾巴 tail,如果不为空,就尝试用 CAS 把新节点塞到尾巴后面,成功就完事大吉;要是失败,或者 tail 为空,那就得靠 enq 方法,它像个耐心的引导员,用自旋 CAS 不断尝试,直到把节点稳妥地插进队尾,确保线程排队万无一失。
一旦进了队列,线程就进入等待状态,等着被唤醒。这时候 parkAndCheckInterrupt 方法就派上用场,它让线程安静地 “睡” 过去,节省 CPU 资源,直到被前驱节点唤醒,或者被中断信号弄醒,醒来还得检查下是不是被中断过,好决定下一步咋办。
2.3 获取 / 释放方法:自定义的关键
获取和释放方法,可是基于 AQS 的并发工具类的灵魂所在,直接决定了线程怎么和资源互动,不同工具类实现起来各有千秋。
就说 ReentrantLock,它的 tryAcquire 方法是获取锁的先锋。非公平锁模式下,线程一上来就不管不顾,先 CAS 操作一把,瞅准 state 是 0,就把它改成 1,代表自己抢到锁,顺便把当前线程标记成独占锁的拥有者;要是 state 已经是 1,再瞅瞅是不是自己已经拿着锁呢,是的话就重入,state 累加,体现可重入特性。公平锁模式就绅士些,先看看队列里有没有前辈在等,没有才去抢,有就乖乖排队。要是 tryAcquire 失败,线程就会被 addWaiter 方法包装成节点送进队列,在 acquireQueued 方法里循环尝试获取锁,像个执着的追求者,直到成功,期间要是被中断,还会记录下来。
再看 Semaphore,它的 tryAcquireShared 方法用来获取共享资源,也就是许可证。线程来要许可证,先看看 state(许可证剩余数量)够不够,够就用 CAS 扣减,成功就拿到许可证,要是不够,或者 CAS 失败,就只能灰溜溜进队列等着,和 ReentrantLock 排队等锁的逻辑类似。releaseShared 方法则是释放许可证,把 state 加回去,还会唤醒那些等许可证的线程,让它们有机会再抢。
CountDownLatch 的 await 方法,相当于线程在等待 “发令枪响”,其实就是检查 state,大于 0 就进队列阻塞,等于 0 就撒欢往前跑,继续执行后续任务。countDown 方法就是 “扣扳机”,每次调用就让 state 减 1,减到 0 就唤醒那些阻塞的线程,大家一起冲。
总之,这些获取和释放方法,都是由具体工具类按照自身需求去精心实现 AQS 里那些抽象的模板方法,灵活运用 state 和队列机制,让线程同步变得井井有条,满足各种复杂的并发场景。
三、源码深度探索
3.1 关键属性:head、tail 与 node
在 AQS 的源码世界里,head 和 tail 就像是队列的 “指挥官” 和 “瞭望兵”,掌控着同步队列的一举一动,而 Node 节点则是队列里的 “小士兵”,每个都肩负着代表线程、传递状态的重任。
先看 head,它是同步队列的头节点,是个 Node 类型的引用,初始值是个空的占位节点,啥线程信息都没有,就像个 “傀儡首领”,但一旦有线程成功获取资源,它就摇身一变,指向真正持有资源的节点,后续节点就眼巴巴瞅着它,等它释放资源来唤醒自己。tail 呢,是尾节点的引用,初始也是 null,随着线程竞争资源失败加入队列,它就像贪吃蛇的尾巴一样,不断往后延伸,始终指向队列里最后一个节点,新进来的线程就往它这儿 “站队”。
Node 类可是个关键 “角色”,作为同步队列和条件队列里的节点,它内部属性丰富得很。除了前面提到的 waitStatus、prev、next 和 thread,还有 nextWaiter,这是在条件队列里用的,指向节点的下一个等待者。比如说,在 ReentrantLock 的条件等待场景下,多个线程因为 await 方法阻塞在条件队列,就靠 nextWaiter 串成一串,和同步队列的双向链表结构互相配合,实现复杂的线程同步逻辑。
从源码里看,addWaiter 方法里创建 Node 节点,根据传入模式(独占或共享)设置相关属性,像下面这段代码:
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;
}
这里清晰展示了新节点入队时,怎么和 tail 交互,利用 CAS 操作安全地把自己挂到队尾,要是 tail 为空,就靠 enq 方法自旋 CAS 初始化队列,确保节点有序入队,让同步队列稳稳运行。
3.2 重要方法源码解析
AQS 里的方法众多,acquire 和 release 这俩堪称 “当家花旦”,一个负责获取资源,一个主管释放资源,在独占模式下的表现尤其精妙。
acquire 方法,是线程抢占资源的 “冲锋号”,源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
它一上来就调用 tryAcquire,这可是个留给子类重写的 “悬念” 方法,像 ReentrantLock 里的非公平锁实现,就会在这方法里用 CAS 抢资源,抢到就把线程标记成独占者,返回 true,线程直接 “通关”;要是返回 false,说明资源被占,线程就得被 addWaiter 方法包装成独占模式的 Node 节点,送进同步队列尾巴,接着在 acquireQueued 方法里陷入自旋。acquireQueued 方法里,线程不断检查前驱节点是不是 head,是 head 就再试一次 tryAcquire,成功就把自己扶正成 head,顺便清理前驱节点引用方便 GC 回收;要是失败,就看 shouldParkAfterFailedAcquire 方法的 “脸色”,这方法判断前驱节点状态,要是 SIGNAL,就安心 parkAndCheckInterrupt 方法里 “睡一觉”,等被唤醒,要是中途被中断,还得记下来,自旋直到抢到资源或者彻底 “凉凉”。
再看 release 方法,这是释放资源、传递 “接力棒” 的关键:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h!= null && h.waitStatus!= 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease 同样由子类实现,像 ReentrantLock 释放锁时,先判断是不是当前独占线程在操作,是就把 state 减回去,减到 0 就彻底释放,唤醒后继节点。要是 tryRelease 成功,就看看 head,不为空且状态不是初始 0,就说明有后继节点眼巴巴等着呢,unparkSuccessor 方法就登场,它先把 head 状态清 0,再找后继节点,跳过那些取消状态的节点,找到就用 LockSupport.unpark 唤醒线程,让同步队列里的资源流转起来,新的线程有机会冲刺获取资源。
在共享模式下,acquireShared 和 releaseShared 方法又有别样精彩。acquireShared 尝试获取共享资源,线程一上来调用 tryAcquireShared,像 Semaphore 里判断许可证够不够,够就扣减返回正数,线程畅行无阻;不够就包装成共享模式 Node 进队,自旋等资源,期间要是前驱是 head 且资源够了,就通过 setHeadAndPropagate 方法不仅自己上位成 head,还可能根据情况唤醒后续共享节点,让资源共享给更多线程。releaseShared 释放共享资源时,先 tryReleaseShared 操作 state,成功就用 doReleaseShared 方法,唤醒后续共享节点,传播资源释放信号,让同步队列里等共享资源的线程们都活跃起来,就像湖面投下石子,泛起层层获取资源的 “涟漪”,满足各种并发场景需求。
四、在 JUC 工具中的应用实例
四、在 JUC 工具中的应用实例
4.1 CountDownLatch:线程协作的利器
CountDownLatch 就像是一场赛跑比赛的发令枪,它能让主线程或者某些线程乖乖等着,直到其他 “选手” 线程都准备就绪,才一起开跑。
从原理上讲,它内部维护的 state 就是那个倒计时计数器。构造 CountDownLatch 时,传入的数字就是初始计数值,比如传入 5,那就意味着得有 5 个线程完成任务,倒计时才结束。线程调用 countDown 方法,就相当于一个选手冲过了终点线,计数器减 1。而那些调用 await 方法的线程呢,就像在起跑线前眼巴巴等着枪响的运动员,处于阻塞状态,一旦计数器归零,“砰”,它们就被唤醒,撒腿狂奔。
来看看示例代码:
import java.util.concurrent.CountDownLatch;
public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
int numThreads = 3;
CountDownLatch latch = new CountDownLatch(numThreads);
for (int i = 0; i < numThreads; i++) {
new Thread(() -> {
try {
// 模拟线程执行任务
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 完成任务");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown(); // 任务完成,计数器减一
}
}).start();
}
latch.await(); // 主线程等待所有任务完成
System.out.println("所有任务都已完成,主线程继续执行");
}
}
在这段代码里,咱们创建了一个初始值为 3 的 CountDownLatch。三个线程启动后,各自 “睡” 1 秒模拟任务执行,完事后调用 countDown。主线程调用 await 阻塞,直到计数器归零,才输出 “所有任务都已完成,主线程继续执行”,完美实现线程间的同步协作,确保任务按序推进。
4.2 Semaphore:资源访问的调控阀
Semaphore 好比是停车场的管理员,控制着同时能进入停车场的车辆数量,也就是管理着共享资源的访问许可。
它的 state 代表可用许可证数量。构造 Semaphore 时指定初始许可证个数,比如设置成 5,那就意味着最多同时允许 5 个线程访问资源。线程调用 acquire 方法,相当于车辆申请进入停车场,许可证够就拿走一个,顺利入场;不够的话,就只能在门口等着,线程阻塞。等线程用完资源,调用 release 方法归还许可证,就像车辆离开停车场,腾出空位,唤醒那些等待的线程来抢许可证。
示例代码如下:
import java.util.concurrent.Semaphore;
public class SemaphoreExample {
public static void main(String[] args) {
int numPermits = 3;
Semaphore semaphore = new Semaphore(numPermits);
for (int i = 0; i < 6; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 获取许可证
System.out.println(Thread.currentThread().getName() + " 获得资源,开始执行");
Thread.sleep(2000); // 模拟资源使用
System.out.println(Thread.currentThread().getName() + " 释放资源");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放许可证
}
}).start();
}
}
}
这里创建了一个许可证数量为 3 的 Semaphore,有 6 个线程嗷嗷叫着要资源。前 3 个线程能顺利拿到许可证,输出 “获得资源,开始执行”,然后 “睡” 2 秒模拟使用资源,接着释放许可证。后面 3 个线程就得等前面的释放了才能拿到许可证干活,通过 Semaphore 巧妙调控资源访问,避免资源被过度争抢导致混乱,保障系统稳定运行。
五、自定义同步器:开启你的并发脑洞
了解了 AQS 这么强大的功能,咱心里肯定痒痒,想不想自己动手搞个专属的同步器呢?其实不难,跟着下面的步骤,你就能开启自定义同步器之旅。
第一步,定义一个继承自 AQS 的同步器类,这就像是打造一个专属的 “魔法工具箱”,比如:
private static class MySync extends AbstractQueuedSynchronizer {
// 这里面放咱们后续要实现的各种方法
}
第二步,在这个同步器类里实现关键方法。像独占模式下,得实现 tryAcquire 和 tryRelease 方法,用来精细控制资源的获取和释放逻辑。就像做一把私人定制的锁,决定啥时候开门,啥时候关门。假设咱们要做个简单的不可重入锁,代码可以这么写:
protected boolean tryAcquire(int acquires) {
assert acquires == 1;
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
protected boolean tryRelease(int releases) {
assert releases == 1;
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
这里 tryAcquire 方法用 CAS 操作去抢资源,抢到就标记当前线程是独占者;tryRelease 方法则是释放资源时,先检查状态,合法就清理独占线程标记,把资源状态归 0。
要是涉及共享模式,就得实现 tryAcquireShared 和 tryReleaseShared 方法,像管理共享的 “宝藏”,规定每个线程能拿多少,啥时候得还回来。
第三步,实现 isHeldExclusively 方法,这能帮咱们判断当前线程是不是独占资源,在一些需要精细控制的场景特别有用,比如要根据独占状态决定后续操作流程。
第四步,要是有额外需求,比如支持条件变量,还得实现对应的方法,像 newCondition,给线程等待资源时提供更多灵活的条件控制选项,就像给排队等公交的人安排不同的候车区域,满足特殊情况需求。
最后,在需要同步的代码块里用咱们自定义的同步器,就像给关键代码穿上一层坚固的 “防护甲”,确保线程安全执行:
private final MySync sync = new MySync();
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
这就是自定义同步器的基本流程,动手试试,你会发现 AQS 的强大远不止于此,能根据各种脑洞大开的需求定制出专属的并发控制利器,让代码在多线程世界里游刃有余。
六、总结
终于,咱们一步步揭开了 AQS 神秘的面纱,从它的基础概念、核心原理,到源码解读,再到实战应用和自定义探索,相信你已经对它有了深刻的认识。
AQS 作为 Java 并发编程的核心框架,就像是一座大厦的基石,支撑起众多强大的并发工具。它通过巧妙的状态管理、高效的队列机制以及灵活的模板方法模式,让线程同步变得有序、高效。无论是日常开发中使用的 ReentrantLock、Semaphore,还是 CountDownLatch,背后都离不开 AQS 的默默付出。
深入学习 AQS,不仅能帮我们在面对高并发场景时,游刃有余地优化代码、解决棘手问题,更是打开了一扇通往 Java 并发编程底层世界的大门,让我们理解那些看似神奇的并发工具是如何构建出来的。这对提升技术深度、拓宽知识边界,以及在面试中脱颖而出,都有着不可估量的价值。
希望这篇文章能成为你学习 AQS 路上的得力助手,激发你继续探索 Java 并发编程的热情。别停下脚步,在多线程的世界里还有更多精彩等待你去发现,快去实践中运用所学,让代码绽放出更耀眼的光芒吧!