从内核队列到网络自旋:信号量、AQS 与分布式锁的跨时代演进
操作系统的课本里,PV 操作是并发控制的起点。
Java 的 JUC 包中,AQS 是无数锁和同步器的脊梁。
分布式系统里,Redis 锁和 ZooKeeper 锁让互斥跨越了机器边界。
它们名字不同、时代不同,但基因里都流淌着 信号量(Semaphore) 的血液。
我是姚Evan,一个在知识汇教育平台的秒杀系统里用过 Redis 分布式锁、在智荟知识库的限流中用 Semaphore、日常写 ReentrantLock 的 Java+AI 学生。今天,我从 操作系统的 PV 操作 出发,一步步追踪到 AQS 再到 分布式锁,画出它们之间那根隐藏的“演化线”。你会发现:锁的本质从未改变,只是实现的舞台从内核内存搬到了网络。
📌 写在前面
大三下学操作系统,老师讲到信号量和 PV 操作时,我盯着那个等待队列的图,忽然觉得眼熟——这不就是 AQS 里的 CLH 队列吗?再联想到秒杀系统里用的 Redis 分布式锁,虽然它用了网络和自旋,但核心逻辑还是“P操作尝试拿资源,V操作释放并唤醒”。于是有了这篇博客:把三个时代的“锁”串起来,看它们如何共用一套思想,又如何在各自环境下妥协与进化。

一、PV 操作:一切锁的“原始模板”
1.1 信号量与 PV 操作
信号量(Semaphore)是一个整数变量,支持两个原子操作:
-
P 操作(proberen,尝试):
s.value--;如果s.value < 0,则当前进程阻塞,进入等待队列。 -
V 操作(verhogen,增加):
s.value++;如果有等待进程,则唤醒一个。
当信号量初始为 1 时,它就是一个 互斥锁。

1.2 内核中的等待队列:高效的阻塞
PV 操作的阻塞是 内核态挂起:进程进入等待队列后,不再占用 CPU。
这是单机环境下的最优解——有操作系统帮忙管理线程的睡眠和唤醒。
二、AQS:Java 把 PV 操作搬进了用户态
2.1 AQS 的“三件套”
AbstractQueuedSynchronizer 是 JUC 的基石,它完美复刻了信号量的思想:
-
volatile int state:相当于信号量的整数值。
-
CLH 队列(双向链表):相当于等待队列,存放等待的线程。
-
CAS 操作:原子修改 state,避免使用内核锁。


2.2 从 ReentrantLock 到 Semaphore
-
ReentrantLock:state表示锁的重入次数(0=未锁,>0=已锁)。 -
Semaphore:state表示剩余许可数,P 操作为acquire(),V 操作为release()。
我在知识汇教育平台的限流场景中,直接用 Semaphore 控制同时处理订单的线程数,代码和 PV 操作几乎一一对应:
Semaphore sem = new Semaphore(10); // 最多 10 个并发
sem.acquire(); // P 操作
try {
processOrder();
} finally {
sem.release(); // V 操作
}
三、分布式锁:PV 操作在网络时代的“变形记”
到了分布式环境,没有共享内存,也没有内核等待队列。
我们需要 把信号量搬到外部服务 里。
3.1 Redis 锁:自旋轮询版 PV
Redis 分布式锁的核心指令:SETNX(set if not exists)和 DEL。
// P 操作(获取锁)
while (!redis.setnx("lock_key", clientId)) {
Thread.sleep(10); // 自旋等待 —— 替代内核阻塞
}
// 临界区
doBiz();
// V 操作(释放锁)
redis.del("lock_key");
差异:
-
阻塞变自旋:没有操作系统帮忙挂起,客户端只能主动轮询,消耗 CPU。
-
需要处理超时、误删等问题(用 Lua 脚本保证原子性)。
3.2 ZooKeeper 锁:真正的“分布式阻塞队列”
ZooKeeper 的临时顺序节点可以实现 高效阻塞:
-
每个客户端在锁节点下创建临时顺序节点。
-
获取子节点列表,如果自己是序号最小的,则获得锁(P 成功)。
-
否则,监听前一个节点的删除事件(相当于进入等待队列,但不轮询)。
-
释放时删除自己的节点(V 操作),触发下一个节点的 watch。
这比 Redis 轮询更接近 PV 的“等待队列”思想——客户端被 ZooKeeper 挂起,不会空转 CPU。

四、我在项目里和“三代锁”交手的故事
-
知识汇秒杀系统:用 Redis 分布式锁控制优惠券库存扣减。高并发下自旋轮询 CPU 飙升,后来改用 Redisson 的
RLock,它内部实现了“自旋 + 订阅”的混合机制,接近阻塞效果。 -
智荟知识库:本地用
Semaphore限制同时调用 Embedding 服务的线程数,防止 OOM。 -
日常开发:
ReentrantLock配合Condition实现生产者-消费者,本质就是 PV 操作的高级封装。
体会:无论是内核队列、AQS 的 CLH 队列,还是 ZooKeeper 的 watch 队列,PV 操作的核心——“资源计数 + 等待队列 + 原子加减”——从未改变。
总结

🤔 思考题:
在分布式锁的实现中,Redis 的 SETNX 轮询会造成 CPU 空转,而 ZooKeeper 的临时顺序节点 + watch 能实现近乎阻塞的效果。但 ZooKeeper 锁的性能(QPS)通常远低于 Redis 锁。
问题:为什么 ZooKeeper 锁虽然“更优雅”,但吞吐量反而不如 Redis 的自旋锁?如果让你设计一个兼顾“低 CPU 开销”和“高吞吐”的分布式锁,你会选择哪种外部存储和算法?
(提示:考虑网络 RTT、watch 的通知风暴、以及 Redis 的 Lua 脚本原子性)
欢迎在评论区留下你的设计方案 —— 下一篇我会聊聊 “从 CAS 到 LongAdder:分段统计如何解决高竞争下的自旋风暴”。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)