Java 从入门到精通(十六):线程通信与 wait()/notify(),为什么有些线程不是抢锁,而是在“等条件”?

news2026/4/15 20:48:08
Java 从入门到精通十六线程通信与 wait()/notify()为什么有些线程不是抢锁而是在“等条件”前一篇我们把线程同步这件事先讲透了为什么多个线程一起改共享变量结果会乱什么是临界区synchronized到底在做什么锁对象为什么必须统一为什么“加锁”本质上是在用排队换正确性但当你继续往下学很快会遇到一个新问题并发世界里线程并不总是在“抢着执行”很多时候它们其实是在“等条件”。比如这些场景仓库里没货消费者线程要先等生产者放货任务还没准备好工作线程不能硬往下跑缓冲区满了生产者不能继续塞数据主线程要等某个结果出现后再继续执行这时你会发现光有synchronized还不够。因为synchronized只能解决同一时刻谁能进临界区多个线程别同时乱改共享数据但它不能直接表达另一层更重要的逻辑现在不是“能不能进”的问题而是“条件满足了吗”。这就是线程通信要解决的事。所以这篇文章我想把 Java 并发入门里非常关键的一块讲透什么叫线程通信它和线程同步有什么区别为什么有些线程要“等待条件”而不是一直抢 CPUwait()、notify()、notifyAll()分别在做什么它们为什么必须和synchronized一起用经典的生产者-消费者模型应该怎么理解初学者最容易踩哪些坑你把这一篇吃透后面再去学LockCondition阻塞队列线程池任务协调CountDownLatch、Semaphore、CyclicBarrier会顺很多。一、先说结论线程通信解决的不是“互斥”而是“条件协调”很多初学者一开始会把同步和通信混在一起。其实它们关注的是两个不同层面。1线程同步关注的是“别同时乱来”比如多个线程一起改同一个变量这时重点是互斥。2线程通信关注的是“什么时候该等什么时候该继续”比如消费者线程发现仓库为空这时它不是要去和别的线程竞争执行资格而是当前条件不满足我先等。等到生产者放入数据再把它叫醒。所以你可以先这样理解同步解决冲突通信解决配合这两个能力经常一起出现但不是一回事。二、为什么不能靠死循环一直等很多新手第一次遇到“等条件”时会很自然地写出这种代码while(!ready){// 一直等}表面上好像能工作。但这种写法问题很大。1会空转占 CPU线程什么都不做却一直检查条件CPU 会被白白消耗掉。2不优雅也不稳定如果很多线程都这么写系统会出现大量无意义轮询。3很难和同步机制正确配合共享条件通常还伴随共享数据访问如果你只是死循环很容易把线程安全和条件等待写乱。所以更合理的做法不是“傻等”而是条件不满足时把线程挂起条件满足时再唤醒它。这正是wait()/notify()的设计目的。三、wait() 到底在做什么你先别急着背规范先抓住最实用的理解。wait()的作用可以概括成一句话当前线程先进入等待状态并释放当前持有的对象锁。这里有两个关键词特别重要。1进入等待状态也就是线程不会继续往下执行了它会暂停等别人来唤醒。2释放锁这点非常关键。如果线程等待时还一直霸占锁别的线程就进不来也就没法修改条件更没法唤醒它。所以wait()不是简单暂停而是我先把锁让出来自己去等待队列里待着等条件变好。这就是为什么它特别适合“有条件地继续执行”的场景。四、notify() 和 notifyAll() 又在做什么既然线程可以等那就必须有人来叫醒它。这就是notify()和notifyAll()的作用。1notify()从等待这个锁对象的线程里随机唤醒一个。注意是“一个”不是全部。2notifyAll()把等待这个锁对象的所有线程都唤醒。注意唤醒不等于立刻执行。被唤醒的线程只是从等待状态转成“有资格继续竞争锁”的状态。最终谁先真的继续执行还要看谁先重新拿到锁。所以这三个方法放在一起看更容易理解wait()我先等notify()叫一个起来notifyAll()大家都起来再竞争五、为什么 wait()/notify() 必须和 synchronized 一起用这是 Java 并发入门里一个特别重要的规则。很多初学者第一次看到会疑惑“既然是线程等待和唤醒为什么非得和锁绑在一起”因为线程通信讨论的通常不是某个孤立线程自己的状态而是围绕同一份共享数据多个线程如何在同一把锁保护下协调条件。更直白一点说条件通常依附在共享数据上共享数据需要同步保护等待和唤醒必须发生在同一套锁语义里所以 Java 才要求线程只有在持有某个对象监视器monitor的前提下才能对这个对象调用wait()、notify()、notifyAll()。如果你不在同步块或同步方法里直接调用通常会抛出java.lang.IllegalMonitorStateException这不是语法刁难而是为了保证线程通信和共享数据访问在同一套规则里成立。六、最基础的写法长什么样先看一个最小可理解例子。假设有一个线程要等任务准备好另一个线程负责把任务准备好。publicclassTaskDemo{privatefinalObjectlocknewObject();privatebooleanreadyfalse;publicvoidwaitForTask(){synchronized(lock){while(!ready){try{lock.wait();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName() 开始处理任务);}}publicvoidprepareTask(){synchronized(lock){readytrue;System.out.println(Thread.currentThread().getName() 已准备好任务);lock.notifyAll();}}}使用时publicclassDemo{publicstaticvoidmain(String[]args){TaskDemotaskDemonewTaskDemo();ThreadworkernewThread(taskDemo::waitForTask,工作线程);ThreadproducernewThread(taskDemo::prepareTask,准备线程);worker.start();try{Thread.sleep(1000);}catch(InterruptedExceptione){e.printStackTrace();}producer.start();}}这里的执行逻辑是工作线程先进入同步块发现ready false调用wait()释放锁并进入等待准备线程进入同步块把ready改成true调用notifyAll()唤醒等待线程工作线程被唤醒后重新竞争锁拿到锁后继续往下执行这就是最基础的“条件等待 条件满足后唤醒”。七、为什么一定要用 while不要用 if这是非常重要、也特别容易被忽视的一个点。很多人一开始会写成if(!ready){lock.wait();}看起来好像也行。但更规范、更安全的写法通常是while(!ready){lock.wait();}为什么1线程被唤醒后条件未必真的满足被叫醒不等于条件一定还成立。因为可能多个线程一起被唤醒其中一个线程先拿到锁并改变了状态当前线程重新拿到锁时条件可能又不满足了2存在伪唤醒spurious wakeup也就是线程可能在没有明确notify的情况下返回等待。虽然初学阶段你不必深究 JVM 细节但规范层面就是要求你用循环重新检查条件。所以一定要建立这个习惯等待不是“醒了就干”而是“醒了先重新看条件”。这就是while的意义。八、经典场景生产者和消费者为什么特别适合讲 wait()/notify()因为它几乎把线程通信的核心逻辑全体现出来了。先看这个场景生产者线程负责往缓冲区放数据消费者线程负责从缓冲区取数据会遇到两个典型条件1缓冲区为空时消费者要等因为没东西可取。2缓冲区满时生产者要等因为不能无限往里塞。这时就不是“谁抢到锁谁干到底”那么简单了而是有货时消费者才能消费有空位时生产者才能生产这就是非常典型的条件协调问题。九、一个简化版的生产者-消费者示例我们先用一个只能存一个整数的仓库来理解。publicclassStore{privateintproduct;privatebooleanhasProductfalse;publicsynchronizedvoidproduce(intvalue){while(hasProduct){try{wait();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}productvalue;hasProducttrue;System.out.println(Thread.currentThread().getName() 生产了value);notifyAll();}publicsynchronizedvoidconsume(){while(!hasProduct){try{wait();}catch(InterruptedExceptione){Thread.currentThread().interrupt();}}System.out.println(Thread.currentThread().getName() 消费了product);hasProductfalse;notifyAll();}}测试代码publicclassDemo{publicstaticvoidmain(String[]args){StorestorenewStore();ThreadproducernewThread(()-{for(inti1;i5;i){store.produce(i);}},生产者);ThreadconsumernewThread(()-{for(inti1;i5;i){store.consume();}},消费者);producer.start();consumer.start();}}这个例子虽然简单但已经把最核心的逻辑表达出来了生产者发现已有产品就等待消费者发现没有产品就等待状态改变后通知对方继续竞争执行这就是线程通信最经典的模型。十、wait() 和 sleep() 看起来都像“暂停”到底差在哪这是入门阶段特别容易混淆的点。很多人觉得它们都能让线程停下来那是不是差不多其实差别很大。1sleep()是 Thread 类的方法谁调用谁休眠到时间自动恢复不会释放锁2wait()是 Object 类的方法必须在同步环境中调用进入等待直到被唤醒或中断会释放当前对象锁这两者的使用场景完全不同。如果你只是想模拟耗时、延迟一下可以用sleep()。如果你是在做线程间条件协调那通常应该考虑wait()。一句话区分就是sleep()是“我先睡会儿”wait()是“条件没到我先把锁让出来等通知”。十一、为什么很多教程更推荐 notifyAll() 而不是 notify()因为notify()虽然更“省”但在多线程场景下更容易出现唤醒错对象的问题。比如同一个锁对象上可能既有生产者在等也有消费者在等。如果你只随机唤醒一个线程结果可能是本来该唤醒消费者却唤醒了另一个生产者对方醒来后发现条件还是不满足又继续等程序虽然不一定立刻错但容易出现协调效率差、逻辑绕、甚至卡住的问题。而notifyAll()的思路是把所有等这个条件的线程都叫起来让它们自己重新竞争锁并检查条件。这样通常更安全更不容易埋下隐蔽 bug。代价是可能有些线程被无效唤醒会多一些上下文切换成本所以工程上常见的建议是初学阶段优先理解和使用notifyAll()等你对等待队列和条件拆分更熟再考虑更细粒度优化十二、初学者最容易踩的 8 个坑1不在同步块里调用 wait()/notify()这会直接抛异常。2用 if 代替 while会让条件检查不稳容易在复杂场景下出错。3调用了 wait() 却忘了对应条件什么时候改变等待和唤醒必须围绕共享状态变化来写不是随便调个 API 就完事。4只写 wait()不写 notify()/notifyAll()那线程可能就一直等下去。5以为 notify() 会立刻让对方执行不对。对方只是被唤醒还得重新竞争锁。6把 sleep() 当成线程通信手段sleep()不能表达“条件满足再继续”的语义只是拖时间。7锁对象不统一如果等待和唤醒不是针对同一个锁对象通信就无法成立。8没有把“条件”和“共享数据”绑定起来思考线程通信不是背方法名而是先问线程到底在等什么条件条件由哪个共享变量表示谁来修改它修改后谁应该被唤醒这才是正确思考顺序。十三、你应该怎么学线程通信才不容易乱如果你现在刚接触这块我建议按这个顺序建立理解。第一步先找“条件”比如缓冲区为空 / 不为空仓库已满 / 未满任务已准备 / 未准备结果已生成 / 未生成第二步再找共享状态这些条件通常不是一句空话而是由具体变量表示readyhasProductsizecount第三步最后才写 wait/notify也就是说你不是为了“用一下 API”而写它们而是为了表达条件不满足 → 进入等待条件变化后 → 唤醒相关线程一旦你这样学就不会把线程通信理解成单纯的语法题。十四、最后总结线程通信的本质不是让线程停下而是让它们学会“按条件配合”如果只用一句话概括这篇我会说线程同步解决的是“别同时乱改”线程通信解决的是“条件没到先等条件到了再继续”。你这篇真正应该带走的是下面这几个核心认识1并发里很多线程不是在抢着执行而是在等待某个条件成立所以通信和同步一样重要。2wait()的核心作用是让线程进入等待并释放锁它不是简单暂停而是为条件协调服务。3notify()/notifyAll()的作用是在条件变化后唤醒等待线程但被唤醒线程还要重新竞争锁。4wait()/notify()必须和synchronized配合使用因为等待、唤醒和共享数据访问必须建立在同一套锁语义下。5等待条件时用 while比用 if 更稳妥因为线程醒来后必须重新检查条件。6生产者-消费者模型是理解线程通信最经典的一块训练场你把它吃透后面很多并发工具都会好懂很多。从这里开始你对 Java 并发的理解就从“会加锁”进入到了“会协调线程配合”的阶段。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2521050.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…