“等待-通知”机制优化(一次性申请)循环等待

article/2025/7/5 0:50:00

1. “等待-通知”机制优化(一次性申请)循环等待

等待‑通知 = 释放锁 + 阻塞 + 唤醒 + 重新抢锁

所有 wait/notify 都属于 锁对象 的等待队列。

用 notifyAll(),写成 while(…) wait() —— 黄金法则。

面对自旋消耗 CPU 的场景,优先考虑等待‑通知替代。

1.1. 为什么要替换“死循环抢锁”

破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待,核心代码如下:

// 一次性申请转出账户和转入账户,直到成功
while(!actr.apply(this, target));

场景

旧方案:自旋 while(!apply());

问题

低冲突、apply()极快

循环几次即可成功

接受

apply 耗时长 / 高并发

可能循环上万次

CPU 空转严重

  • 改进思路:条件不满足→阻塞线程;条件满足→唤醒线程。这就是 等待‑通知。

现实类比——“就医流程”

  1. 挂号+分诊 → 线程尝试获取互斥锁
  2. 大夫让去检查 → 条件不满足
  3. 患者做检查,释放诊室 → 线程 wait(),释放锁
  4. 检查完再分诊 → 被 notify 唤醒后重新抢锁
  5. 再次就诊 → 条件已满足,继续执行
  6. 要点:释放锁 ➜ 进入等待队列 ➜ 条件满足 ➜ 通知 ➜ 重新获取锁

1.2. 用 synchronized 实现等待 - 通知机制

1. java的等待-通知(以账户转账为例)

  1. 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()

方法

唤醒谁

风险

notify()

随机 1 线程

可能唤错对象,导致真正需要的线程永远唤不起(“饿死”)

notifyAll()

所有等待线程

唤醒后再竞争锁,更安全

  • 除非经过严格证明,否则 默认用 notifyAll()

http://www.hkcw.cn/article/vDBcIjbxvf.shtml

相关文章

麒麟信安安装谷歌浏览器

参考文档 麒麟信安系统Chrome离线安装包&#xff1a;高效便捷的浏览器解决方案-CSDN博客 项目文件预览 - 麒麟信安系统Chrome离线安装包:本仓库提供了一个适用于麒麟信安系统的Chrome浏览器离线安装包。该安装包包含了所有必要的依赖文件&#xff0c;并且已经对系统中已有的依…

智启未来:当知识库遇见莫奈的调色盘——API工作流重构企业服务美学

目录 引言 一、初识蓝耘元生代MaaS平台 1.1 平台架构 1.2 平台的优势 1.3 应用场景 二、手把手教你如何在蓝耘进行注册 &#xff08;1&#xff09;输入手机号&#xff0c;将验证码正确填入即可快速完成注册 &#xff08;2&#xff09;进入下面的页面表示已经成功注册&…

LangGraph framework

目录 Agent 架构Router&#xff08;路由&#xff09;工具调用代理&#xff08;Tool-calling Agent&#xff09;工具调用记忆机制&#xff08;Memory&#xff09;规划机制&#xff08;Planning&#xff09; 自定义 Agent 架构人类参与&#xff08;Human-in-the-loop&#xff09;…

零基础开始的网工之路第十七天------计算机网络知识

目录 1、以太网MAC地址 2.交换机的工作模式 2.1、交换机以太网接口双工模式 2.2、交换机以太网接口速率 4.模式间的转换 5.命令行的层次关系 6.命令行帮助 7.常用命令 8.交换机以太网接口的工作模式配置 二.路由器配置 三.TCP协议 2.TCP的建立连接和断开连接 2.1建…

核心机制:滑动窗口

TCP 协议 1.确认应答 可靠传输的核心机制 2.超时重传 可靠传输的核心机制 3.连接管理 TCP/网络 最高的面试题 三次握手,建立连接(必须是 三次) 四次挥手,断开连接(可能是 三次) 核心机制四:滑动窗口 算法中的"滑动窗口" 出自 TCP 前面的三个…

JAVA核心知识点--元注解详解

&#x1f4da;博客主页&#xff1a;代码探秘者 ✨专栏&#xff1a;《JavaSe》 其他更新ing… ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;作者水平有限&#xff0c;欢迎各位大佬指点&…

微软markitdown PDF/WORD/HTML文档转Markdown格式软件整合包下载

本次和大家分享另一个微软发布的非常热门的文件文档转Markdown格式文档的软件markitdown&#xff0c;软件可以将PDF&#xff0c;word&#xff0c;ppt&#xff0c;Excel等十几种格式文档转换为markdown格式文档&#xff0c;我基于当前最新0.1.2版本制作了免安装一键启动整合包。…

(八)登录认证与学生写作画像

本次将赵昱琨同学之前完成的学生写作画像与智能学习路径规划的后端与目前已有的后端框架进行整合。同时为了实现学生写作画像与智能学习路径规划&#xff0c;需要在之前简易的登录系统上进行重构&#xff0c;所以本次大规模重写了登录模块&#xff0c;同时发现很多过去冗余的代…

【agent开发】部署LLM(一)

本周基本就是在踩坑&#xff0c;没什么实质性的进展 下载模型文件 推荐一个网站&#xff0c;可以简单计算下模型推理需要多大显存&#xff1a;https://apxml.com/tools/vram-calculator 我的显卡是RTX 4070&#xff0c;有12GB的显存&#xff0c;部署一个1.7B的Qwen3应该问题…

【Linux】pthread多线程基础

参考博客&#xff1a;https://blog.csdn.net/Alkaid2000/article/details/128121066 线程概述 与进程类似&#xff0c;线程(thread)是允许应用程序并发执行多个任务的一种机制。一个进程可以包含多个线程。同一个程序中的所有线程均会独立执行相同程序&#xff0c;且共享同一份…

瑞萨CS+ for CC V8.13.00环境安装教程

前言&#xff1a;最近接触到瑞萨的芯片&#xff0c;需要安装对应的集成开发环境&#xff0c;发现这与ARM内核的单片机存在很大的不同&#xff0c;这里先简单介绍一下其IDE的安装配置方式。 1&#xff0c;官网下载 瑞萨半导体开发环境安装网址 CS | Renesas 当然在下载安装包之…

【知识点】第3章:基本数据类型

文章目录 知识点整理数字类型字符类型 练习题判断题程序题 知识点整理 数字类型 Python语言提供整数、浮点数、复数3种数字类型。 不同进制的引导符号&#xff1a; 不考查进制间的转换。 浮点数类型与数学中实数的概念一致&#xff0c;表示带有小数的数值。Python语言要求所…

【算法】回溯法

一、回溯法的基本思想 回溯法有“通用解题方法”的美称&#xff0c;解题过程是一个搜索过程。在搜索尝试过程中寻找问题的解&#xff0c;当发现已不满足求解条件时&#xff0c;就“回溯”返回&#xff08;也就是递归返回&#xff09;&#xff0c;尝试别的路径。因此&#xff0…

AIGC 基础篇 高等数学篇 01函数与极限

声明&#xff1a;本文章仅用于博主本人复习&#xff0c;请不要将本文章当成预习篇或者讲解篇 此外&#xff0c;此文章不会包含全部的高等数学知识&#xff0c;仅仅是为了学习AI而进行的前期学习&#xff0c;因此知识含量不会很多&#xff0c;由于博主是第一次尝试做&#xff0…

如何在 Windows 11 Home 版上下载和安装 Hyper-V

Windows 11 Home 版与之前的微软操作系统版本一样,没有自带 Hyper-V 管理器。因此,如果您想在 Windows 11 Home 上下载和安装 Hyper-V,以下是详细的步骤教程。 Hyper-V 是微软提供的一种虚拟化解决方案,允许用户为各种操作系统创建虚拟机。与 VMware 或 VirtualBox 不同,…

C++ --- string类的简单实现

string类的简单实现 前言1、基本成员2、构造方法和析构方法2.1无参构造2.2有参构造2.3析构函数2.4拷贝构造函数 3、遍历方式3.1operator [ ]3.2iterator3.2.1正向迭代器3.2.2const正向迭代器 3.3范围for 4、常用方法&#xff0c;运算符重载c_str()size()reverse()push_back()po…

ESP32之Linux编译环境搭建流程

背景&#xff1a;为了解决 “windows环境中编译ESP32代码速度慢” 的问题&#xff0c;现搭建一个Linux环境&#xff0c;让windows下的VScode连接到Linux环境&#xff0c;VSCode负责编辑代码&#xff0c;虚拟机用于编译代码。 目录 一、安装VMware 1.1 获取VMware安装包 1.2…

Python-matplotlib中的Pyplot API和面向对象 API

matplotlib中的Pyplot API和面向对象 API Pyplot API&#xff08;状态机模式&#xff09;面向对象 API 详解二者差别核心区别方法命名差异注意事项差别举例 &#x1f345; Pyplot API&#xff08;状态机模式&#xff09;和面向对象 API 是两种不同的编程接口.&#x1f345; 它们…

BUUCTF之[ACTF2020 新生赛]BackupFile

打开环境就一句话 找出源文件! 结合题目名字&#xff1a;BackupFile 先用dirsearct扫描网站文件 发现一个index.php.bak ,拼接url下载 打开发现php代码 <?php include_once "flag.php";if(isset($_GET[key])) {$key $_GET[key];if(!is_numeric($key)) {exit…

Spring Boot 3.X 下Redis缓存的尝试(一):初步尝试

背景 想像一下有这么一个场景&#xff0c;一个系统有超多角色、角色下有多个菜单、菜单下有多个按钮权限&#xff0c;这种子父级关系查询每次向数据库查询相当耗时&#xff0c;那么我们是否可以将这种更新频次不高&#xff0c;而查询耗时的数据且不直接影响业务的数据放进缓存中…