欢迎光临
我们一直在努力

AQS 到底是什么?用银行窗口和共享厕所把它讲透

如果你去翻 Java 并发包的源码,会发现一个名字反复出现——AbstractQueuedSynchronizer,简称 AQS。ReentrantLock 靠它,Semaphore 靠它,CountDownLatch 靠它,ReentrantReadWriteLock 也靠它。

但很多人看完源码还是一头雾水:这玩意儿到底在干嘛?

这篇文章不讲源码,用两个生活场景,把 AQS 彻底讲明白。


先想象一个场景:银行只有一个办事窗口

假设银行只有 1 个办事窗口,但来了 100 个客户,大家都要办业务。

如果没有规则,会发生什么?

  • 大家一拥而上,挤在窗口前
  • 有人插队,有人被挤开
  • 有人办到一半被另一个人打断
  • 最后谁也没办好,场面混乱

所以银行需要一套规则:

  • 窗口只能一个人用
  • 先来的人先办
  • 没轮到的人去排队区等着
  • 前一个人办完离开后,叫下一个人
  • 被叫到的人再回到窗口办理

AQS 就是 Java 里的这套"窗口 + 排队 + 叫号"机制。


把 AQS 拆开看,就很像这个银行

银行场景AQS 里的东西作用
窗口是否空闲 state 表示资源当前状态,比如锁没被占用、被占用几次
客户 线程 想使用资源的线程
排队区 FIFO 等待队列 没抢到资源的线程先进队列等待
叫号 unpark 唤醒 资源释放后,唤醒队列里的下一个线程
客户坐下等待 park 阻塞 线程抢不到资源就先暂停,不浪费 CPU

所以 AQS 不是某一个锁,而是一个通用排队框架。


为什么需要 AQS?

如果没有 AQS,每次写一个锁,程序员都要自己处理这些麻烦事:

  • 怎么判断资源有没有被占用?
  • 抢不到资源怎么办?
  • 抢不到的线程怎么排队?
  • 排队时怎么让线程睡觉,而不是死循环消耗 CPU?
  • 资源释放后,怎么唤醒下一个线程?
  • 多个线程同时抢,怎么保证不出错?

这些事情非常复杂,而且很多锁都需要做。

所以 Doug Lea 把这些公共逻辑抽出来,做成了 AQS:

  • AQS 负责:排队、阻塞、唤醒、状态管理
  • 子类负责:定义"什么算抢到资源""什么算释放资源"

这就是经典的模板方法模式——框架把通用流程定好,具体业务逻辑交给子类实现。


再举一个更生活化的例子:共享厕所

假设一个楼层只有一个厕所。

第一个进去的人把门锁上,这时 state 从 0 变成 1,表示"厕所被占用"。

后面的人来了:

  • 发现门锁着
  • 不能硬闯
  • 只能在门口排队
  • 排队时不能一直敲门,否则很烦,应该先去做别的事
  • 里面的人出来后,通知排队第一个人
  • 第一个人进去,继续锁门

如果厕所变成 3 个隔间,那就可以同时进 3 个人。这就有点像 Semaphore:资源不是 1 个,而是多个许可。

AQS 厉害的地方就在这里:

  • 它既能表示"只能一个人用"
  • 也能表示"可以有多个许可"
  • 还能表示"等所有任务完成再放行"

为什么要设计 AQS?

因为 Java 并发包里很多工具,本质上都在解决同一类问题:

多个线程竞争有限资源时,怎么公平、安全、高效地排队和使用资源?

比如:

  • ReentrantLock:锁只能被一个线程持有
  • Semaphore:只允许一定数量的线程同时访问
  • CountDownLatch:等几个事件都完成后,再放行
  • ReentrantReadWriteLock:读可以多个,写只能一个

它们表面不一样,但底层都有:

  • 一个资源状态
  • 一批等待的线程
  • 一套抢资源、排队、唤醒的规则

所以 AQS 就是把这些共同逻辑统一起来,避免重复造轮子。


一句话记住 AQS

AQS 就像 Java 并发世界里的:

"资源窗口 + 排队叫号系统"

state 是窗口状态,等待队列是排队区,线程抢不到资源就去排队睡觉,资源释放后再被叫醒。


补充:AQS 的内部结构长什么样?

虽然前面没讲源码,但了解 AQS 的内部结构有助于加深理解。AQS 的核心就三样东西:

state(资源状态)

一个 volatile int 变量,不同子类赋予它不同含义:

  • ReentrantLock:0 表示空闲,1 表示被占用,大于 1 表示重入次数
  • Semaphore:表示剩余许可数量
  • CountDownLatch:表示剩余计数
FIFO 等待队列

一个双向链表,每个节点(Node)代表一个排队的线程。没抢到资源的线程会被封装成 Node,挂到队列尾部,然后阻塞自己。

park / unpark

底层依赖 LockSupport 类。park 让线程暂停(相当于去排队区坐着等),unpark 唤醒线程(相当于叫号)。

state = 0(空闲)
→ 线程A来抢,CAS 把 state 改成 1,抢到资源,直接执行

state = 1(被A占用)
→ 线程B来抢,CAS 失败
→ B 被封装成 Node,加入等待队列尾部
→ B 调用 park(),线程挂起,不消耗 CPU

线程A执行完毕,释放资源
→ CAS 把 state 改回 0
→ 从队列头部取出线程B的 Node
→ 调用 unpark(B),唤醒线程B
→ 线程B重新尝试抢资源


一张图总结

┌──────────────────────────┐
│ AQS │
│ ┌────────────────────┐ │
│ │ state │ │ ← 资源状态(volatile int)
│ └────────────────────┘ │
│ ┌────────────────────┐ │
│ │ FIFO 等待队列 │ │ ← 排队区(双向链表)
│ │ [B] → [C] → [D] │ │
│ └────────────────────┘ │
│ ┌────────────────────┐ │
│ │ park / unpark │ │ ← 阻塞 / 唤醒
│ └────────────────────┘ │
└──────────┬───────────────┘

┌────────────────┼────────────────┐
▼ ▼ ▼
ReentrantLock Semaphore CountDownLatch
(独占资源) (共享资源) (计数放行)

AQS 提供了通用的排队、阻塞、唤醒机制,子类只需要告诉它"怎么算抢到"和"怎么算释放",就能实现各种不同的并发工具。这就是 AQS 的设计精髓。

赞(0)
未经允许不得转载:171主机测评 » AQS 到底是什么?用银行窗口和共享厕所把它讲透
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址