AQS共享锁的传播机制精髓

一、背景:共享模式 vs 独占模式

  • 独占模式(Exclusive) :如 ReentrantLock,一次只允许一个线程持有锁。
    • 释放时:只需唤醒队列中的下一个等待者(调用 unparkSuccessor(head) 即可)。
  • 共享模式(Shared) :如 SemaphoreCountDownLatchReadWriteLock 的读锁。
    • 允许多个线程同时持有锁(只要资源足够)。
    • 所以:一旦资源可用,可能要连续唤醒多个等待线程!

这就引出了 "传播(propagation)" 的概念。


二、核心问题:如何确保"所有能获取资源的线程都被唤醒"?

假设:

  • 当前有 3 个线程在 AQS 队列中等待共享锁(A → B → C)。
  • 此时资源释放了(比如信号量 permit +1)。
  • 线程 A 被唤醒并成功获取资源。
  • 但此时资源仍然充足(比如 permit 还剩 1),那么 B 也应该被唤醒!

但如果只在释放时唤醒一次(像独占锁那样),B 和 C 就可能永远等下去,即使资源已经可用。

👉 因此,共享模式必须支持"级联唤醒"或"传播唤醒"


三、PROPAGATE 和 SIGNAL 的作用

🔹 Node.SIGNAL(-1)

  • 含义:当前节点的后继节点需要被唤醒
  • 这是 AQS 中通用的状态,独占和共享都用
  • 当一个节点入队后,会把前驱设为 SIGNAL,表示"我等着,你释放时记得叫我"。

🔹 Node.PROPAGATE(-3)

  • 仅用于共享模式
  • 含义:即使当前没有明确的"需要唤醒"的信号,也要继续传播释放动作
  • 它是一种"保险机制",防止在并发竞争下漏掉唤醒。

四、为什么需要 PROPAGATE?------ 并发场景下的"信号丢失"问题

考虑这个竞态条件(race condition)

  1. 线程 T1 调用 releaseShared(),准备唤醒后继。
  2. 此时队列 head 是 H,H 的 waitStatus 是 0(因为刚被设置为 head,还没来得及设 SIGNAL)。
  3. T1 检查到 ws == 0,于是尝试 CAS 把它设为 PROPAGATE(表示:"虽然现在没信号,但我要记录这次释放,以便后续传播")。
  4. 与此同时 ,另一个线程 T2 成功获取了共享锁,并调用 setHeadAndPropagate,把新节点设为 head。
  5. 如果没有 PROPAGATE,T1 可能认为"没人需要唤醒",直接退出,导致后面的线程无法被唤醒!

PROPAGATE 的作用就是:在状态不确定时,留下一个"释放发生过"的标记,确保后续操作能继续传播唤醒。


五、代码逻辑精解

setHeadAndPropagate(node, propagate)

  • propagate > 0:表示 tryAcquireShared 返回正数,说明还有剩余资源,应该继续唤醒别人。

  • 条件判断很"保守"(conservative):

    java 复制代码
    if (propagate > 0 || h == null || h.waitStatus < 0 || ...)

    只要有任何迹象表明可能需要传播 ,就调用 doReleaseShared()

  • 特别注意:h.waitStatus < 0 包括 SIGNAL(-1)PROPAGATE(-3),都表示"需要关注后继"。

doReleaseShared()

这是一个 自旋 + CAS 的传播循环

java 复制代码
for (;;) {
    Node h = head;
    if (h != null && h != tail) {
        int ws = h.waitStatus;
        if (ws == SIGNAL) {
            // 正常情况:后继需要唤醒
            CAS(SIGNAL → 0); unparkSuccessor(h);
        }
        else if (ws == 0) {
            // 没有明确信号,但可能是并发释放!
            // 设为 PROPAGATE,留下"释放已发生"的标记
            CAS(0 → PROPAGATE);
        }
    }
    // 如果 head 没变,说明稳定了,可以退出
    if (h == head) break;
}

💡 关键思想:即使当前看不出需要唤醒谁,也要通过 PROPAGATE 确保"释放事件"不会丢失


六、举个实际例子:Semaphore

java 复制代码
Semaphore sem = new Semaphore(1);
// 三个线程同时调用 sem.acquire()
// 初始 permit = 1,只有第一个能成功,其他两个入队等待。

sem.release(); // permit 变回 1
  • release() → 调用 releaseShared()doReleaseShared()
  • 唤醒第一个等待线程(A)
  • A 获取 permit 后,发现 propagate = 0(因为 permit 用完了),不传播
  • 但如果 release() 被调用了两次(permit=2),则:
    • 第一次唤醒 A,A 获取后 permit=1 → propagate=1 → 继续传播
    • 触发第二次 doReleaseShared(),唤醒 B

如果没有传播机制,B 就卡住了!


七、总结:精髓所在

概念 作用
传播(Propagation) 共享模式下,一次释放可能需唤醒多个线程
SIGNAL (-1) 明确指示"后继需要唤醒"
PROPAGATE (-3) 在状态模糊时,防止释放信号丢失的保险机制
自旋 + CAS 循环 应对高并发下的状态竞争,确保最终一致性

设计哲学

"宁可多唤醒几次(unnecessary wake-ups),也不能漏掉一次该唤醒的线程。"

------ 这就是 AQS 共享模式的鲁棒性所在。


附加:Node.waitStatus 的几个值

常量 含义
0 --- 初始状态
-1 SIGNAL 后继需要被唤醒
-2 CONDITION 在 Condition 队列中
-3 PROPAGATE 共享模式下,表示应继续传播释放
1 CANCELLED 节点已取消

希望这能帮你彻底理解 AQS 共享模式的"传播"机制!

相关推荐
似水明俊德10 小时前
02-C#.Net-反射-面试题
开发语言·面试·职场和发展·c#·.net
Leinwin10 小时前
OpenClaw 多 Agent 协作框架的并发限制与企业化规避方案痛点直击
java·运维·数据库
薛定谔的悦10 小时前
MQTT通信协议业务层实现的完整开发流程
java·后端·mqtt·struts
enjoy嚣士11 小时前
springboot之Exel工具类
java·spring boot·后端·easyexcel·excel工具类
Thera77711 小时前
C++ 高性能时间轮定时器:从单例设计到 Linux timerfd 深度优化
linux·开发语言·c++
罗超驿11 小时前
独立实现双向链表_LinkedList
java·数据结构·链表·linkedlist
炘爚11 小时前
C语言(文件操作)
c语言·开发语言
阿蒙Amon11 小时前
C#常用类库-详解SerialPort
开发语言·c#
盐水冰12 小时前
【烘焙坊项目】后端搭建(12) - 订单状态定时处理,来单提醒和顾客催单
java·后端·学习
凸头12 小时前
CompletableFuture 与 Future 对比与实战示例
java·开发语言