“等待-通知”机制优化(一次性申请)循环等待
1. “等待-通知”机制优化(一次性申请)循环等待
等待‑通知 = 释放锁 + 阻塞 + 唤醒 + 重新抢锁
所有 wait/notify 都属于 锁对象 的等待队列。
用 notifyAll(),写成 while(…) wait() —— 黄金法则。
面对自旋消耗 CPU 的场景,优先考虑等待‑通知替代。
1.1. 为什么要替换“死循环抢锁”
在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待,核心代码如下:
// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target));
场景 | 旧方案:自旋 | 问题 |
低冲突、 | 循环几次即可成功 | 接受 |
apply 耗时长 / 高并发 | 可能循环上万次 | CPU 空转严重 |
- 改进思路:条件不满足→阻塞线程;条件满足→唤醒线程。这就是 等待‑通知。
现实类比——“就医流程”
- 挂号+分诊 → 线程尝试获取互斥锁
- 大夫让去检查 → 条件不满足
- 患者做检查,释放诊室 → 线程 wait(),释放锁
- 检查完再分诊 → 被 notify 唤醒后重新抢锁
- 再次就诊 → 条件已满足,继续执行
- 要点:释放锁 ➜ 进入等待队列 ➜ 条件满足 ➜ 通知 ➜ 重新获取锁
1.2. 用 synchronized 实现等待 - 通知机制
1. java的等待-通知(以账户转账为例)
-
import java.util.ArrayList; import java.util.List;class Allocator {// 已被占用的资源集合private final List<Object> als = new ArrayList<>();/** 一次性申请所有资源:拿不到就等待,直到成功 */public synchronized void apply(Object from, Object to) {// 经典 while‑wait 写法,防止“条件曾经满足但现在又不满足”while (als.contains(from) || als.contains(to)) {try {wait(); // 让出锁并阻塞} catch (InterruptedException e) {// 若想支持中断,可在此处处理中断逻辑Thread.currentThread().interrupt();}}// 条件满足:注册资源,占有它们als.add(from);als.add(to);}/** 归还资源并通知等待线程 */public synchronized void free(Object from, Object to) {als.remove(from);als.remove(to);notifyAll(); // 唤醒所有等待在此锁上的线程} }class Account {private static final Allocator ACTR = new Allocator(); // 单例分配器private int id; // 如果需要防死锁的顺序锁仍可保留private int balance;public Account(int id, int balance) {this.id = id;this.balance = balance;}/** 从 this 向 target 转账 amt 元 */public void transfer(Account target, int amt) {// ① 一次性申请两个账户(可能阻塞)ACTR.apply(this, target);try {// ② 细粒度锁:先锁 this,再锁 targetsynchronized (this) {synchronized (target) {if (this.balance >= amt) {this.balance -= amt;target.balance += amt;}}}} finally {// ③ 归还资源,不管成功或异常都要释放ACTR.free(this, target);}}/* 下面是辅助方法,仅用于调试/演示 */public int getBalance() { return balance; }public int getId() { return id; } }
Account a = new Account(1, 1000);
Account b = new Account(2, 1000);Thread t1 = new Thread(() -> a.transfer(b, 100));
Thread t2 = new Thread(() -> b.transfer(a, 200));t1.start();
t2.start();
t1.join();
t2.join();System.out.printf("A=%d, B=%d%n", a.getBalance(), b.getBalance());
2. python的等待-通知机制(以账户转账为例)
import threading
import time
from typing import Listclass Allocator:"""负责一次性分配两份“资源”(这里就是两个 Account 对象)。内部用 Condition 同时完成互斥和等待‑通知工作。"""def __init__(self):self._held: List[object] = []self._cv = threading.Condition() # 包含一把隐式的 RLockdef apply(self, a: object, b: object) -> None:"""阻塞直到同时拿到 a、b 两个资源"""with self._cv: # ① 加锁while a in self._held or b in self._held:self._cv.wait() # ② 条件不满足 → 释放锁并阻塞# 条件满足;登记资源self._held.extend((a, b))def free(self, a: object, b: object) -> None:"""归还资源并通知所有等待线程"""with self._cv:for res in (a, b):if res in self._held:self._held.remove(res)self._cv.notify_all() # 唤醒因 apply() 而等待的线程class Account:_allocator = Allocator() # 单例分配器def __init__(self, acct_id: int, balance: int = 0):self.id = acct_idself.balance = balanceself._lock = threading.Lock() # 细粒度账户锁def transfer(self, target: "Account", amt: int):# ① 一次性申请两把账户锁(可能阻塞)Account._allocator.apply(self, target)try:# ② 加细粒度锁,真正修改余额# 为防止死锁,这里仍固定顺序(id 小的先锁)first, second = (self, target) if self.id < target.id else (target, self)with first._lock:with second._lock:if self.balance >= amt:self.balance -= amttarget.balance += amtfinally:# ③ 归还资源Account._allocator.free(self, target)# --------------------- 简单演示 ---------------------
if __name__ == "__main__":a = Account(1, 1000)b = Account(2, 1000)def worker(from_acct, to_acct, amount, loops=100):for _ in range(loops):from_acct.transfer(to_acct, amount)t1 = threading.Thread(target=worker, args=(a, b, 5))t2 = threading.Thread(target=worker, args=(b, a, 5))t1.start(); t2.start()t1.join(); t2.join()print(f"A:{a.balance}, B:{b.balance}") # 理论上仍然各 1000
用 synchronized 实现等待 - 通知机制的流程:
1. 在下面这个图里,左边有一个等待队列,同一时刻,只允许一个线程进入 synchronized 保护的临界区(这个临界区可以看作大夫的诊室),当有一个线程进入临界区后,其他线程就只能进入图中左边的等待队列里等待(相当于患者分诊等待)。这个等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列。
2. 在并发程序中,当一个线程进入临界区后,由于某些条件不满足,需要进入等待状态,Java 对象的 wait() 方法就能够满足这种需求。如上图所示,当调用 wait() 方法后,当前线程就会被阻塞,并且进入到右边的等待队列中,这个等待队列也是互斥锁的等待队列。 线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁后,其他线程就有机会获得锁,并进入临界区了。
3. 那线程要求的条件满足时,该怎么通知这个等待的线程呢?很简单,就是 Java 对象的 notify() 和 notifyAll() 方法。我在下面这个图里为你大致描述了这个过程,当条件满足时调用 notify(),会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过。
曾经满足:因为notify() 只能保证在通知时间点,条件是满足的。而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。需要格外注意。
notify()
vs notifyAll()
方法 | 唤醒谁 | 风险 |
| 随机 1 线程 | 可能唤错对象,导致真正需要的线程永远唤不起(“饿死”) |
| 所有等待线程 | 唤醒后再竞争锁,更安全 |
- 除非经过严格证明,否则 默认用
notifyAll()
。