AQS:Java并发包里的"包租公",管理着你的锁和通行证!🔑
一个曾试图手写公平锁,结果写出"史诗级BUG"的Java码农。现在,我选择相信AQS。🙏
朋友们,今天我们来聊聊Java并发世界里那位低调但无处不在的"扫地僧" ------AbstractQueuedSynchronizer,江湖人称 AQS。
如果你用过ReentrantLock、Semaphore、CountDownLatch,觉得它们好用又神奇,那你已经享受了AQS的"福报"。因为这些神器的核心引擎,都是这位"包租公"在默默驱动。
一、AQS是啥?一个"万能同步器模板"!
想象一下,你想在小区里搞个共享单车停放点,规则是:
- 车位有限,比如5个。
- 有车的人才能停(获取资源),没车的人得排队等。
- 有人骑走一辆(释放资源),队头的哥们才能进去停。
你会怎么实现这个规则?
自己手写?搞个volatile int表示剩余车位,再用个List<Thread>当等待队列?然后处理各种边界情况、并发竞争、线程唤醒... 光是想想,头发已经开始掉了。💇
AQS站出来说:"别折腾了!这套'资源管理+排队'的通用流程,我帮你实现了!"
AQS的核心就是一个同步器框架,它为你准备好了两样东西:
- 一个
int state:表示"资源数量"。比如state=5表示有5个车位。你可以自定义state的含义(是锁的重入次数?还是信号量的许可数?随你定)。 - 一个FIFO双向链表队列 :这就是那个"等待队列"。没抢到资源的线程,乖乖进去排队睡觉(
park),等资源释放了再被唤醒。
说白了,AQS就是一个"包租公" :
- 他手里有个小本本(
state),记录着还有几套房(资源)可租。 - 门口有个排队机(CLH队列),没租到房的客户(线程)就在这排队等。
- 他定义了租房、退房的基本流程,但具体的"租房资格审核规则"(比如,是"先到先得"还是"VIP优先"),由你这个"房东"来定。
二、为啥要用它?因为"重复造轮子,轮子砸自己脚"!
在AQS出现之前,每个大神想实现一个新锁或同步工具,都得从头写一遍:状态维护 + 线程排队 + 阻塞唤醒 + 处理竞争。不仅容易出错,而且代码复杂到亲妈都不认识。
AQS解决了什么?
它把同步器最复杂、最容易出错的公共部分 (线程排队、阻塞、唤醒、公平性调度)抽出来,用精妙的、经过千锤百炼的代码实现好了。你只需要关注业务核心逻辑 :如何定义"获取资源"和"释放资源"的规则。
这就像:
- 以前:你想开餐馆,得自己盖楼、拉水电、装修、招厨师、设计菜单... 累死。
- 现在(用AQS) :AQS给了你一个精装修的商铺模板 ,水电管网、排烟消防全做好了。你只需要决定卖火锅还是卖烧烤(定义资源规则) ,然后摆上桌椅厨具就能开业!🚀
三、怎么用?继承它,然后"填空"!
AQS是抽象类,你用它的方式就是继承它,并实现几个"钩子方法"。最重要的是这两个:
protected boolean tryAcquire(int arg):尝试获取资源 。规则你定!成功返回true,失败返回false。protected boolean tryRelease(int arg):尝试释放资源 。规则你定!成功返回true。
还有用于共享模式的tryAcquireShared和tryReleaseShared。
AQS的骨架伪代码(极度简化版):
scss
// 你继承的这个"包租公"内部,有一个核心的"获取资源"流程
public final void acquire(int arg) {
// 1. 先问问你定的规则:现在能拿到资源吗?(调用你重写的tryAcquire)
if (!tryAcquire(arg)) {
// 2. 没拿到?那把自己加到等待队列尾部
Node node = addWaiter(Node.EXCLUSIVE);
// 3. 进入一个循环:不断检查自己是不是队头,是就再tryAcquire一下,不是就安心睡觉(park)
for (;;) {
if (node.prev == head && tryAcquire(arg)) { // 是队头,再试试
setHead(node);
return;
}
// 不是队头或者还没抢到,就找地方安心睡觉,等前驱节点唤醒
parkAndCheckInterrupt();
}
}
}
// release也类似,释放成功后,会去唤醒队列中合适的线程
看明白了吗? 复杂的"排队-检查-睡觉-唤醒"循环,AQS帮你包了!你只需要告诉它: "什么情况下算获取成功?"(tryAcquire的逻辑)。
四、实战:用AQS手搓一个"非公平锁"
我们来实现一个最简陋的MyLock,体验一下当"房东"的感觉。
java
// 注意:这是演示版,工业级请用ReentrantLock!
public class MyLock {
// 请出我们的"包租公"!
private final Sync sync = new Sync();
// 核心:继承AQS,定义我们的规则
private static class Sync extends AbstractQueuedSynchronizer {
// 规则1: 获取锁。state为0表示锁空闲,为1表示被占用。
@Override
protected boolean tryAcquire(int acquires) {
// 经典CAS操作:如果state是0,就把它改成1,表示我抢到了!
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread()); // 记录锁被谁占了
return true; // 成功!
}
return false; // 抢失败了
}
// 规则2: 释放锁
@Override
protected boolean tryRelease(int releases) {
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException(); // 不是锁的持有者,想啥呢?
}
setExclusiveOwnerThread(null); // 清空持有者
setState(0); // 资源恢复空闲
return true;
}
// 额外:这算是个锁吧,所以提供个条件查询
protected boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}
}
// 对外的锁接口
public void lock() {
sync.acquire(1); // 调用AQS定义好的"获取"模板流程!
}
public void unlock() {
sync.release(1); // 调用AQS定义好的"释放"模板流程!
}
}
看到了吗? 我们只写了核心的资源竞争规则 (用CAS抢state),而线程排队、阻塞、唤醒这些脏活累活,全交给sync.acquire()和sync.release()了,这都是AQS父类里现成的!
五、工作中的注意事项:别把"包租公"用成"背锅侠"!
-
"别直接继承AQS!"(在绝大多数情况下)
AQS是给并发大师们造轮子用的 (比如Doug Lea本人用来造
ReentrantLock)。我们普通码农,**99.9%的情况应该直接用ReentrantLock、Semaphore、CountDownLatch** 这些现成的、完美的轮子。别为了炫技去手写一个自定义同步器,除非你是框架开发者。 -
理解"公平"与"非公平"
- 非公平锁 (默认):
tryAcquire时,不管有没有人排队,新来的线程直接尝试"插队"抢资源。吞吐量高,但可能"饿死"排队久的线程。 - 公平锁 :
tryAcquire时,会先检查队列里有没有人在等,有的话就乖乖去排队。更讲武德,但性能略有损耗。 - 大部分场景,用默认的非公平锁就好,性能至上。
- 非公平锁 (默认):
-
小心"状态"的设计
AQS里的
state是个int,别乱用。设计自定义同步器时,要精确规划state的每一位表示什么。比如ReentrantLock用state表示重入次数,Semaphore用它表示剩余许可数。别把state当成万能垃圾桶,什么都往里塞。
六、该怎么恰当地用?站在巨人的肩膀上!
- 日常开发 :忘记你需要继承AQS这件事。直接、熟练地使用
ReentrantLock、Semaphore、CountDownLatch、CyclicBarrier、ReentrantReadWriteLock。它们都是基于AQS构建的、久经考验的"瑞士军刀"。 - 阅读源码 :当你想深入理解这些工具的原理时,再去啃它们的源码。你会发现,它们内部都有一个
Sync类继承自AQS,然后分别实现了公平/非公平的tryAcquire和tryRelease。这是学习并发设计的绝佳教材。 - 知识储备 :知道AQS的存在和其"模板方法"的设计模式,能让你在面试时侃侃而谈,在工作中遇到诡异并发问题时,多一个深入分析的视角。 "哦,这个锁可能是AQS队列挂住了" ------能说出这句话,你已经超过很多同行了。
结语
AQS是Java并发王国的基石,是隐藏在java.util.concurrent包众多神器背后的"无名英雄"。它把最脏、最累、最易错的"线程排队管理"活揽了下来,只留给你一个简单的接口去定义"资源竞争规则"。
所以,让我们对这些底层大师们保持敬意。下次当你流畅地用lock.lock()和lock.unlock()时,记得心里默念一句: "谢谢AQS包租公!" 🎩
(现在,你可以愉快地去用ReentrantLock了,把继承AQS的冲动,留给真正的框架作者们吧。)