当前位置: 首页 > news >正文

Java并发——线程间的通信

在多线程编程中,线程间通信是一个核心话题。当多个线程需要协同完成某个任务时,它们必须能够互相通知状态的变化,以避免竞态条件和无效的资源占用。Java提供了多种线程间通信的方式,从最基础的wait/notify机制,到Lock配合Condition的灵活方案。本文将带你全面了解线程间通信的原理、常见陷阱以及如何优雅地实现线程协作。

一、线程间通信的必要性

思考一个简单的场景:两个线程操作一个共享变量,一个线程负责加1,另一个线程负责减1,要求交替执行10轮。如果没有通信机制,线程A可能连续加多次,线程B才减一次,导致结果混乱。线程间通信正是为了解决这类问题——让线程在合适的时机暂停和唤醒,从而保证操作的顺序性和数据的一致性。

二、传统的wait/notify机制

2.1 基本使用

Java中每个对象都有一组监视器方法:wait()notify()notifyAll()。它们必须在同步块(synchronized)中使用,因为需要获取对象的监视器锁。

下面是一个经典的“生产者-消费者”示例,两个线程交替对变量进行+1和-1操作:

class ShareData { private int number = 0; public synchronized void increment() throws InterruptedException { // 1. 判断 if (number != 0) { this.wait(); } // 2. 干活 number++; System.out.println(Thread.currentThread().getName() + " => " + number); // 3. 通知 this.notifyAll(); } public synchronized void decrement() throws InterruptedException { if (number != 1) { this.wait(); } number--; System.out.println(Thread.currentThread().getName() + " => " + number); this.notifyAll(); } } public class WaitNotifyDemo { public static void main(String[] args) { ShareData data = new ShareData(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.increment(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) { try { data.decrement(); } catch (InterruptedException e) { e.printStackTrace(); } } }, "B").start(); } }

运行结果会交替输出A => 1B => 0,共10轮。这里的关键点在于:

  • 线程在执行操作前,先判断条件是否满足(number是否为0或1)。

  • 不满足则调用wait()进入等待状态,同时释放锁

  • 操作完成后,调用notifyAll()唤醒所有等待的线程。

2.2 虚假唤醒问题

当我们将线程数增加到4个(两个加线程,两个减线程),并运行多次后,可能会看到23等异常值,甚至出现负数。这是因为if判断导致的虚假唤醒

虚假唤醒指的是线程被唤醒后,条件可能已经不再满足,但程序仍然继续执行。例如,当number为0时,A1和A2都等待在increment方法中;当B执行减1后调用notifyAll(),A1和A2同时被唤醒,它们都从wait()后继续执行,导致number被连续加了两次,变为2。

解决方案:将if改为while,使线程被唤醒后重新检查条件。这是JDK文档明确要求的。

public synchronized void increment() throws InterruptedException { while (number != 0) { // 使用while this.wait(); } number++; System.out.println(Thread.currentThread().getName() + " => " + number); this.notifyAll(); }

2.3 wait/notify的局限性

  • 无法精确唤醒notifyAll()会唤醒所有等待线程,增加了不必要的上下文切换;notify()只唤醒一个,但无法指定唤醒哪一个。

  • 必须与synchronized绑定:只能配合synchronized使用,不够灵活。

  • 无法响应中断wait()会抛出InterruptedException,但线程无法在等待期间主动中断。

三、Lock + Condition:更灵活的通信方式

从JDK 1.5开始,java.util.concurrent.locks包提供了Lock接口和Condition接口,弥补了wait/notify的不足。

3.1 Condition的基本用法

每个Condition对象都相当于一个“队列”,通过await()signal()/signalAll()实现线程的等待与唤醒。与wait/notify类似,使用前必须先获取对应的锁。

将上面的例子用ReentrantLockCondition改写:

import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class ShareData { private int number = 0; private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); public void increment() throws InterruptedException { lock.lock(); try { while (number != 0) { condition.await(); } number++; System.out.println(Thread.currentThread().getName() + " => " + number); condition.signalAll(); } finally { lock.unlock(); } } public void decrement() throws InterruptedException { lock.lock(); try { while (number != 1) { condition.await(); } number--; System.out.println(Thread.currentThread().getName() + " => " + number); condition.signalAll(); } finally { lock.unlock(); } } }

相比synchronizedLock提供了更多控制能力(如tryLock、可中断锁等),而Condition则可以创建多个等待队列,实现精确唤醒

3.2 多个Condition实现精准通信

需求:三个线程 A、B、C 依次执行,A 打印5次,B 打印10次,C 打印15次,循环10轮。

这种场景下,需要在线程A执行完后精确唤醒B,B执行完后精确唤醒C,C执行完后精确唤醒A。通过为每个线程创建一个Condition对象,并结合一个状态标识,即可轻松实现。

import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class ShareResource { private int flag = 1; // 1: A, 2: B, 3: C private final Lock lock = new ReentrantLock(); private final Condition conditionA = lock.newCondition(); private final Condition conditionB = lock.newCondition(); private final Condition conditionC = lock.newCondition(); public void print5() { lock.lock(); try { while (flag != 1) { conditionA.await(); } for (int i = 1; i <= 5; i++) { System.out.println(Thread.currentThread().getName() + " => " + i); } flag = 2; conditionB.signal(); // 唤醒B } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print10() { lock.lock(); try { while (flag != 2) { conditionB.await(); } for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + " => " + i); } flag = 3; conditionC.signal(); // 唤醒C } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void print15() { lock.lock(); try { while (flag != 3) { conditionC.await(); } for (int i = 1; i <= 15; i++) { System.out.println(Thread.currentThread().getName() + " => " + i); } flag = 1; conditionA.signal(); // 唤醒A } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } } public class ConditionDemo { public static void main(String[] args) { ShareResource resource = new ShareResource(); new Thread(() -> { for (int i = 0; i < 10; i++) resource.print5(); }, "A").start(); new Thread(() -> { for (int i = 0; i < 10; i++) resource.print10(); }, "B").start(); new Thread(() -> { for (int i = 0; i < 10; i++) resource.print15(); }, "C").start(); } }

这样,每个线程只会在属于自己的标识位被设置时才执行,执行完后精确唤醒下一个线程,避免了无效的唤醒竞争。

四、经典面试题:交替打印数字和字母

题目:两个线程,一个打印1~52的数字,另一个打印A~Z的字母,要求打印结果为12A34B...5152Z。

分析:数字线程每次打印两个数字,字母线程每次打印一个字母。可以通过一个标志位来控制切换,也可以用Condition来实现精确交替。

4.1 使用 wait/notify 实现

class Printer { private int num = 1; private char letter = 'A'; private boolean printNum = true; public synchronized void printNumber() { for (int i = 0; i < 26; i++) { while (!printNum) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(num++); System.out.print(num++); printNum = false; notifyAll(); } } public synchronized void printLetter() { for (int i = 0; i < 26; i++) { while (printNum) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.print(letter++); printNum = true; notifyAll(); } } } public class PrintDemo { public static void main(String[] args) { Printer printer = new Printer(); new Thread(printer::printNumber).start(); new Thread(printer::printLetter).start(); } }

4.2 使用 Condition 实现

import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; class Printer { private int num = 1; private char letter = 'A'; private boolean printNum = true; private final Lock lock = new ReentrantLock(); private final Condition numberCondition = lock.newCondition(); private final Condition letterCondition = lock.newCondition(); public void printNumber() { lock.lock(); try { for (int i = 0; i < 26; i++) { while (!printNum) { numberCondition.await(); } System.out.print(num++); System.out.print(num++); printNum = false; letterCondition.signal(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } public void printLetter() { lock.lock(); try { for (int i = 0; i < 26; i++) { while (printNum) { letterCondition.await(); } System.out.print(letter++); printNum = true; numberCondition.signal(); } } catch (InterruptedException e) { e.printStackTrace(); } finally { lock.unlock(); } } }

五、总结与最佳实践

  1. 优先使用Lock+Condition
    如果需要精确控制线程唤醒顺序、支持中断或超时,或者需要更灵活的锁机制,推荐使用ReentrantLockCondition

  2. 避免虚假唤醒
    无论使用wait/notify还是Condition.await(),判断条件时必须使用while循环,而不是if

  3. finally中释放锁
    Lock.unlock()必须放在finally块中,确保锁在任何情况下都能被释放,避免死锁。

  4. 使用多个Condition实现精确通信
    当需要多个线程协作时,为每个线程创建独立的Condition,结合状态标志,可以显著提高代码的可读性和效率。

  5. 注意notify()vsnotifyAll()
    使用Condition.signal()可以精确唤醒一个等待线程,而signalAll()会唤醒所有等待该条件的线程。一般情况下,精确唤醒能减少不必要的上下文切换。

http://www.jsqmd.com/news/530116/

相关文章:

  • 车联网感知技术
  • 新能源车空调PTC加热器拆解:为什么你的电动车暖风来得快?
  • COMSOL磁铁磁感线分布与电感计算模型下的永磁铁电磁场分析
  • 《Windows 11 从入门到精通》读书笔记 3.4.3:时间和日期的调整——我用“看日历 + 自动/手动切换 + 立即同步”把时间校准到位
  • 老旧Mac图形性能优化终极指南:告别卡顿,重获流畅体验
  • 二中机房一败涂地(1.0)
  • 蛋白质配体分析工具PLIP完全使用指南
  • TeXMe:如何在3分钟内创建自渲染的Markdown+LaTeX文档?
  • 深度学习:Vision Transformer (ViT):算法原理、架构解构
  • 作业三:个人主页
  • 【AI大模型春招面试题8】词元化(Tokenization)的作用是什么?BPE、WordPiece、Unigram的原理与优缺点?
  • 5-Compose开发-Modifier进阶
  • 如何优雅解锁付费内容?智能访问工具的完整指南
  • 从匿名管道到 Master-Slave 进程池:Linux 进程间通信深度实践
  • ControlNet-v1-1_fp16实战指南:模型适配与图像生成全流程优化
  • espeak-ng语音合成终极指南:快速掌握127种语言免费TTS技术
  • 嵌入式图形开发实战:Adafruit GFX库从问题到解决方案的完整指南
  • Guohua Diffusion 嵌入式开发联动:Keil5工程展示AI生成UI界面素材
  • 仅限首批MCP认证伙伴内部流出:OAuth 2026架构设计图原始版(含签名链路、密钥轮转SOP与审计日志字段规范)
  • 车辆信号震动信号的滤波、幅值与能量分析——基于测试台采集文件的研究
  • MVME 300A 64-W5882B01B单板计算机
  • Qwen3-VL-WEBUI效果展示:上传草图秒生成HTML代码,实测惊艳
  • 拒绝手绘贴图地狱!AIGC联动:写实3D白模秒转“绝区零”风赛博二次元角色
  • ROCm在Ubuntu 24.04上的深度解析与完整安装指南
  • 解决CODESYS RTE与EtherCAT主站版本不匹配问题:从报错到成功配置的全过程
  • Qwen-Image-Lightning快速部署指南:一键启动,极简界面专注创意
  • Qwen3-VL-2B-Instruct一文详解:内置WEBUI如何高效调用
  • 数论知识-----质因数分解(竞赛必会)
  • 无名杀:打造你的专属三国杀网页游戏体验
  • 如何彻底解决微信QQ撤回消息的烦恼?RevokeMsgPatcher完整防撤回指南