Java并发编程(04):线程间通信,等待/通知机制
本文源码:GitHub·点这里 || GitEE·点这里
一、概念简介
1、线程通信
在操作系统中,线程是个独立的个体,但是在线程执行过程中,如果处理同一个业务逻辑,可能会产生资源争抢,导致并发问题,通常使用互斥锁来控制该逻辑。但是在还有这样一类场景,任务执行是有顺序控制的,例如常见的报表数据生成:

- 启动数据分析任务,生成报表数据;
- 报表数据存入指定位置数据容器;
- 通知数据搬运任务,把数据写入报表库;
该场景在相对复杂的系统中非常常见,如果基于多线程来描述该过程,则需要线程之间通信协作,才能有条不紊的处理该场景业务。
2、等待通知机制
如上的业务场景,如果线程A生成数据过程中,线程B一直在访问数据容器,判断该过程的数据是否已经生成,则会造成资源浪费。正常的流程应该如图,线程A和线程B同时启动,线程A开始处理数据生成任务,线程B尝试获取容器数据,数据还没过来,线程B则进入等待状态,当线程A的任务处理完成,则通知线程B去容器中获取数据,这样基于线程等待和通知的机制来协作完成任务。
3、基础方法
等待/通知机制的相关方法是Java中Object层级的基础方法,任何对象都有该方法:
- notify:随机通知一个在该对象上等待的线程,使其结束wait状态返回;
- notifyAll:唤醒在该对象上所有等待的线程,进入对象锁争抢队列中;
- wait:线程进入waiting等待状态,不会争抢锁对象,也可以设置等待时间;
线程的等待通知机制,就是基于这几个基础方法。
二、等待通知原理
1、基本原理
等待/通知机制,该模式下指线程A在不满足任务执行的情况下调用对象wait()方法进入等待状态,线程B修改了线程A的执行条件,并调用对象notify()或者notifyAll()方法,线程A收到通知后从wait状态返回,进而执行后续操作。两个线程通过基于对象提供的wait()/notify()/notifyAll()等方法完成等待和通知间交互,提高程序的可伸缩性。
2、实现案例
通过线程通信解决上述数据生成和存储任务的解耦流程。
public class NotifyThread01 {
static Object lock = new Object() ;
static volatile List<String> dataList = new ArrayList<>();
public static void main(String[] args) throws Exception {
Thread saveThread = new Thread(new SaveData(),"SaveData");
saveThread.start();
TimeUnit.SECONDS.sleep(3);
Thread dataThread = new Thread(new AnalyData(),"AnalyData");
dataThread.start();
}
// 等待数据生成,保存
static class SaveData implements Runnable {
@Override
public void run() {
synchronized (lock){
while (dataList.size()==0){
try {
System.out.println(Thread.currentThread().getName()+"等待...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("SaveData .."+ dataList.get(0)+dataList.get(1));
}
}
}
// 生成数据,通知保存
static class AnalyData implements Runnable {
@Override
public void run() {
synchronized (lock){
dataList.add("hello,");
dataList.add("java");
lock.notify();
System.out.println("AnalyData End...");
}
}
}
}
注意:除了dataList满足写条件,还要在AnalyData线程执行通知操作。
三、管道流通信
1、管道流简介
基本概念
管道流主要用于在不同线程间直接传送数据,一个线程发送数据到输出管道,另一个线程从输入管道中读取数据,进而实现不同线程间的通信。
实现分类
管道字节流:PipedInputStream和PipedOutputStream;
管道字符流:PipedWriter和PipedReader;
新IO管道流:Pipe.SinkChannel和Pipe.SourceChannel;
2、使用案例
public class NotifyThread02 {
public static void main(String[] args) throws Exception {
PipedInputStream pis = new PipedInputStream();
PipedOutputStream pos = new PipedOutputStream();
// 链接输入流和输出流
pos.connect(pis);
// 写数据线程
new Thread(new Runnable() {
public void run() {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
// 将从键盘读取的数据写入管道流
PrintStream ps = new PrintStream(pos);
while (true) {
try {
System.out.print(Thread.currentThread().getName());
ps.println(br.readLine());
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}, "输入数据线程:").start();
// 读数据线程
new Thread(new Runnable() {
public void run() {
BufferedReader br = new BufferedReader(new InputStreamReader(pis));
while (true) {
try {
System.out.println(Thread.currentThread().getName() + br.readLine());
} catch (IOException e) {
e.printStackTrace();
}
}
}
}, "输出数据线程:").start();
}
}
写线程向管道流写入数据,读线程读取数据,完成基本通信流程。
四、生产消费模式
1、业务场景
基于线程等待通知机制:实现工厂生产一件商品,通知商店卖出一件商品的业务流程。
2、代码实现
public class NotifyThread03 {
public static void main(String[] args) {
Product product = new Product();
ProductFactory productFactory = new ProductFactory(product);
ProductShop productShop = new ProductShop(product);
productFactory.start();
productShop.start();
}
}
// 产品
class Product {
public String name ;
public double price ;
// 产品是否生产完毕,默认没有
boolean flag ;
}
// 产品工厂:生产
class ProductFactory extends Thread {
Product product ;
public ProductFactory (Product product){
this.product = product;
}
@Override
public void run() {
int i = 0 ;
while (i < 20) {
synchronized (product) {
if (!product.flag){
if (i%2 == 0){
product.name = "鼠标";
product.price = 79.99;
} else {
product.name = "键盘";
product.price = 89.99;
}
System.out.println("产品:"+product.name+"【价格:"+product.price+"】出厂...");
product.flag = true ;
i++;
// 通知消费者
product.notifyAll();
} else {
try {
// 进入等待状态
product.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
// 产品商店:销售
class ProductShop extends Thread {
Product product ;
public ProductShop (Product product){
this.product = product ;
}
@Override
public void run() {
while (true) {
synchronized (product) {
if (product.flag == true ){
System.out.println("产品:"+product.name+"【价格"+(product.price*2)+"】卖出...");
product.flag = false ;
product.notifyAll(); //唤醒生产者
} else {
try {
product.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
流程描述:ProductFactory生成一件商品,通知商店售卖,通过flag标识判断控制是否进入等待状态,商店卖出商品后,再次通知工厂生产商品。
五、源代码地址
GitHub·地址
https://github.com/cicadasmile/java-base-parent
GitEE·地址
https://gitee.com/cicadasmile/java-base-parent

| 序号 | 文章标题 |
|---|---|
| 01 | Java并发:线程的创建方式,状态周期管理 |
| 02 | Java并发:线程核心机制,基础概念扩展 |
| 03 | Java并发:多线程并发访问,同步控制 |
Java并发编程(04):线程间通信,等待/通知机制的更多相关文章
- java并发编程实战《六》等待-通知机制
用"等待-通知"机制优化循环等待 前言 在破坏占用且等待条件的时候,如果转出账本和转入账本不满足同时在文件架上这个条件,就用死循环的方式来循环等待. 1 // 一次性申请转出账户和 ...
- Java 线程间通信 —— 等待 / 通知机制
本文部分摘自<Java 并发编程的艺术> volatile 和 synchronize 关键字 每个处于运行状态的线程,如果仅仅是孤立地运行,那么它产生的作用很小,如果多个线程能够相互配合 ...
- Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...
- 19、Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
Java并发编程:线程间协作的两种方式:wait.notify.notifyAll和Condition 在前面我们将了很多关于同步的问题,然而在现实中,需要线程之间的协作.比如说最经典的生产者-消费者 ...
- Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)
Java并发编程系列: Java 并发编程:核心理论 Java并发编程:Synchronized及其实现原理 Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) Java 并发编程 ...
- 《java多线程编程核心技术》不使用等待通知机制 实现线程间通信的 疑问分析
不使用等待通知机制 实现线程间通信的 疑问分析 2018年04月03日 17:15:08 ayf 阅读数:33 编辑 <java多线程编程核心技术>一书第三章开头,有如下案例: ...
- Java多线程编程(6)--线程间通信(下)
因为本文的内容大部分是以生产者/消费者模式来进行讲解和举例的,所以在开始学习本文介绍的几种线程间的通信方式之前,我们先来熟悉一下生产者/消费者模式. 在实际的软件开发过程中,经常会碰到如下场景 ...
- 【转】Java 并发编程:线程间的协作(wait/notify/sleep/yield/join)
一.线程的状态 Java中线程中状态可分为五种:New(新建状态),Runnable(就绪状态),Running(运行状态),Blocked(阻塞状态),Dead(死亡状态). New:新建状态,当线 ...
- Java多线程编程核心技术---线程间通信(二)
通过管道进行线程间通信:字节流 Java提供了各种各样的输入/输出流Stream可以很方便地对数据进行操作,其中管道流(pipeStream)是一种特殊的流,用于在不同线程间直接传送数据,一个线程发送 ...
- Java多线程编程核心技术---线程间通信(一)
线程是操作系统中独立的个体,但这些个体如果不经过特殊处理就不能成为一个整体.线程间的通信就是成为整体的必用方案之一.线程间通信可以使系统之间的交互性更强大,在大大提高CPU利用率的同时还会使程序员对各 ...
随机推荐
- testNG 问题总结
1. Eclipse中TestNG报告乱码问题 在eclipse 安装根目录下的eclipse.ini 文件,在最后增加 -Dfile.encoding=UTF-8
- python 基础篇 模块化
在做项目的时候,虽然你不可能把全世界的代码都放到一个文件夹下,但是类似模块化的思想还是要有的--那就是以项目的根目录作为最基本的目录,所有的模块调用,都要通过根目录一层层向下索引的方式来 import ...
- 一年时间,Pipenv就成为Python官方推荐的顶级工具?
Pipenv是Kenneth Reitz在一年多前创建的“面向程序员的Python开发工作流程”,现在已成为管理软件包依赖关系的Python官方推荐资源. Python软件包安装管理的简要历史 为了正 ...
- NFS共享设置
一.安装所需要的软件环境[root@fengling ~]# yum -y install rpcbind nfs-utils二.创建共享目录mkdir /web三.启动服务[root@fenglin ...
- HDU 6341 Let Sudoku Rotate
#include<bits/stdc++.h> using namespace std; #define rep(i,a,b) for(int i=a;i<=b;++i) #defi ...
- 如何在mysql中实现自然排序
背景 熟悉mysql的同学应该清楚,mysql在对字符串做order by排序时是按照字典序进行排序的,但是如果字符串中包含数字的话(我们称这种类型的字符串为alphanumeric),仅按照字典序的 ...
- matlab画图(一)
例1.画出函数图像 >> x=-pi/2:0.01:pi/2; >> y=x+sin(x)+exp(x); >> plot(x,y,'r','Linewidth', ...
- WeChatSampleBuilder V2.0 使用教程(网页版+桌面版)
为了方便开发者可以快速搭建一个最小化所需模块的 Senparc.Weixin SDK Sample 项目,我们于 2018 年 11 月发布了首个 WeChatSampleBuilder 的版本,受到 ...
- 解决Chrome插件安装时出现的“程序包无效”问题
问题原因,新版的插件头部文件的修改,导致旧版的插件包无法使用,解决方式:解压. 1.把下载后的.crx扩展名的离线Chrome插件的文件扩展名改成.zip或者.rar(如果看不到Chrome插件的扩展 ...
- 数学--数论--HDU 12151七夕节
七夕节 Problem Description 七夕节那天,月老来到数字王国,他在城门上贴了一张告示,并且和数字王国的人们说:"你们想知道你们的另一半是谁吗?那就按照告示上的方法去找吧!&q ...