java线程间通信:一个小Demo完全搞懂
版权声明:本文出自汪磊的博客,转载请务必注明出处。
Java线程系列文章只是自己知识的总结梳理,都是最基础的玩意,已经掌握熟练的可以绕过。
一、从一个小Demo说起
上篇我们聊到了Java多线程的同步机制:Java多线程同步问题:一个小Demo完全搞懂。这篇我们聊一下java多线程之间的通信机制。
上一篇探讨java同步机制的时候我们举得例子输出log现象是:一段时间总是A线程输出而另一段时间总是B线程输出,有没有一种方式可以控制A,B线程交错输出呢?答案是当然可以了,这时候我们就要用到多线程的wait/notify机制了。
wait/notify机制就是当线程A执行到某一对象的wait()方法时,就会进入等待状态,此时线程A放弃持有的锁,其余线程可以竞争锁的持有权。当有其余线程调用notify()或者notifyAll()方法的时候就可能(当有多个线程的时候notify()方法只会唤醒处于等待状态线程中的一个)唤醒线程A,使其从wait状态醒来,继续向下执行业务逻辑。
接下来,我们通过一个小demo加以理解。
二、单生产者消费者模式
demo很简单,就是开启两个线程,一个生产面包,另一个负责消费面包,并且生产一个就要消费一个,交替执行。
首先看下BreadFactory类:
public class BreadFactory {
//生产面包个数计数器
private int count = 0;
//线程的锁
private Object o = new Object();
private boolean flag = false; public void product() {
synchronized (o) {
if (flag) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"生产了第" + (++count) + "个面包");
flag = true;
o.notify();
}
} public void consume() {
synchronized (o) {
if (!flag) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"消费第" + count + "个面包");
flag = false;
o.notify();
}
}
}
此类就是负责生产,消费面包,flag主要用于控制线程之间的切换。
接下来我们看下Producter,Consumer类:
public class Producter extends Thread { private BreadFactory mBreadFactory; public Producter(BreadFactory mBreadFactory) {
super();
this.mBreadFactory = mBreadFactory;
} @Override
public void run() {
//
while (true) {
mBreadFactory.product();
}
}
}
很简单,初始化的时候需要传递进来一个BreadFactory实例对象,线程启动的时候调用BreadFactory类中product()方法不停生产面包。
Consumer类同理:
public class Consumer extends Thread { private BreadFactory mBreadFactory; public Consumer(BreadFactory mBreadFactory) {
super();
this.mBreadFactory = mBreadFactory;
} @Override
public void run() {
//
while (true) {
mBreadFactory.consume();
}
}
}
最后看下main方法:
public static void main(String[] args) {
//
BreadFactory factory = new BreadFactory();
Producter p1 = new Producter(factory);
p1.start();
Consumer c1 = new Consumer(factory);
c1.start();
}
没什么要多说的,就是初始化并启动线程,运行程序,输出如下:
Thread-0生产了第1个面包
Thread-1消费第1个面包
Thread-0生产了第2个面包
Thread-1消费第2个面包
Thread-0生产了第3个面包
Thread-1消费第3个面包
Thread-0生产了第4个面包
Thread-1消费第4个面包
。。。。。
三、多生产者消费者模式
似乎很顺利的就实现了啊,但是实际需求中怎么可能只有一个生产者,一个消费者,生产者,消费者是有多个的,我们试下多个生产者,消费者是什么现象,修改main中逻辑:
public static void main(String[] args) {
//
BreadFactory factory = new BreadFactory();
Producter p1 = new Producter(factory);
p1.start();
Consumer c1 = new Consumer(factory);
c1.start();
Producter p2 = new Producter(factory);
p2.start();
Consumer c2 = new Consumer(factory);
c2.start();
}
我们就是只多添加了一个生产者和一个消费者,其余没任何变化。
运行程序,输出信息如下:
。。。
Thread-2生产了第4个面包
Thread-1消费第4个面包
Thread-2生产了第5个面包
Thread-1消费第5个面包
Thread-2生产了第6个面包
Thread-1消费第6个面包
Thread-3消费第6个面包
Thread-0生产了第7个面包
Thread-3消费第7个面包
。。。
咦?生产到第6个面包,竟然被消费了两次,这显然是不正常的,那是哪里出问题了呢?
四、多生产者消费者模式问题产生原因分析
接下来,我们直接分析问题产生的原因,我们分析下BreadFactory中product()与consume()方法:
public void product() {
synchronized (o) {
if (flag) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"生产了第" + (++count) + "个面包");
flag = true;
o.notify();
}
} public void consume() {
synchronized (o) {
if (!flag) {
try {
o.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"消费第" + count + "个面包");
flag = false;
o.notify();
}
}
从线程启动顺序以及打印信息可以看出线程0,线程2负责生产面包,线程1,线程3负责消费面包。
线程执行过程中,线程1消费掉第5个面包,此时flag置为false,执行notify()方法唤醒其余线程争取锁获取执行权。
此时线程3获取线程执行权,执行consume()业务逻辑flag此时为false,进入if(!flag)逻辑,执行wait()方法,此时线程3进入wait状态,停留在25行代码处。释放锁资源,其余线程可以争取执行权。
此时线程1获取执行权,和线程3一样,最终停留在25行代码处。释放锁资源,其余线程可以争取执行权。注意:此时线程1,线程3都停留在25行代码处,处于wait状态。
接下来线程2获取执行权,执行生产业务,生产了第6个面包,然后释放锁资源,其余线程可以争取执行权。
然后线程1又获取执行权,上面说了线程1停留在25行代码处,现在获取执行权从25行代码处开始执行,消费掉第6个面包没问题,flag置为false。然后释放锁资源,其余线程可以争取执行权。
此时线程3又获取执行权,上面分析时说了线程3处于25行代码处wait状态,现在获取执行权从25行代码处开始执行,又消费了第6个面包,到这里面包6被消耗了两次。
经过上面分析已经知道产生问题的原因了,线程获取执行权后直接从wait处开始继续执行,不在检查if条件是否成立,这里就是问题产生的原因了。
那怎么修改的呢?很简单了,将if判断改为while条件判断就可以了,这样线程获取执行权后还会再次检查while条件判断是否成立。
运行程序打印Log如下:
。。。
Thread-1消费第19个面包
Thread-0生产了第20个面包
Thread-1消费第20个面包
Thread-2生产了第21个面包
看输出Log上面问题是解决了,生产一个面包只会消费一次,但是发现程序运行自己终止了,上面生产到第21个面包程序似乎不运行了没Log输出了,这是什么原因呢?
五、notify()通知丢失问题以及notify()与notifyAll()的区别
要想明白上述问题产生的原因我们就必须搞懂notify()与notifyAll()的区别。简单说就是notify()只会唤醒同一监视器处于wait状态的一个线程(随机唤醒),
而notifyAll()会唤醒同一监视器处于wait状态的所有线程。
我们分析上面问题产生的原因:线程0,线程2负责生产面包,线程1,线程3负责消费面包,在程序运行过程存在如下情况:
线程1,3处于consume()中的wait()处,线程0处于product()中wait()处,此时线程2生产完第21个面包执行notify()方法,通知处于同一监视器下处于wait状态线程,此时处于wait状态线程为线程1,线程3与线程0,按理说我们是想唤醒一个线程1,3中一个线程来消费刚刚生产的面包,但是程序可不知道啊,调用notify方法随机唤醒一个线程,碰巧此时唤醒的还是生产线程0,这就是notify通知丢失问题,线程0执while判断又处于wait状态了,到这里就出现了控制台没有Log输出现象了,经过上面分析我们该明白问题出现的原因就是notify通知丢失问题,通知了一个我们不想通知的线程,那怎么解决呢?很简单了,程序中notify()方法改为notifyAll()就可以了,改为notifyAll()方法上述线程2通知的时候会一起唤醒线程0,1,3,也就是唤醒同一监视器处于wait状态的所有线程,到这里运行程序就没有什么问题了。
六、notify()与notifyAll()性能问题
也许有些同学有疑问了,既然notify()方法会产生问题,那我就用notifyAll()不就完了,直接屏蔽掉notify()方法。这样做当然是很Low的做法。
假设有N个线程在wait状态下,调用notifyall会唤醒所有线程,然后这N个线程竞争同一个锁,最后只有一个线程能够得到锁,其它线程又回到wait状态。这意味每一次唤醒操作可能带来大量的竞争锁的请求。这对于频繁的唤醒操作而言性能上可能是一种灾难。如果说总是只有一个线程被唤醒后能够拿到锁,这种情况下使用notify的性能是要高于notifyall的。
七、JDK1.5中Condition通知机制
JDK1.5中Condition通知机制这里就不详细讲解了,Condition中await(),signal(),signalAll()相当于传统线程通信机制中wait(),notify(),notifyAll()方法。
我们修改BreadFactory类如下,其余类均不变:
public class BreadFactory {
// 生产面包个数计数器
private int count = 0;
// 线程的锁
private Lock lock = new ReentrantLock();
private Condition consumeCon = lock.newCondition();
private Condition productCon = lock.newCondition();
private boolean flag = false; public void product() {
lock.lock();
try {
while (flag) {
try {
productCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "生产了第"
+ (++count) + "个面包");
flag = true;
consumeCon.signal();
} finally {
//
lock.unlock();
}
} public void consume() {
lock.lock();
try {
while (!flag) {
try {
consumeCon.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "消费第" + count
+ "个面包");
flag = false;
productCon.signal();
} finally {
//
lock.unlock();
}
}
}
其强大之处就在于代码中6,7,15,28,40,53行代码处,我们并没有调用signalAll()方法,而是调用的signal()方法。
这样我们就可以控制在生产完一个面包去唤醒消费的线程来消费面包,而不用连同生产线程一起唤醒,这就是其强大之处,这里就不详细分析了,不太熟悉的同学可自行搜索其余博客学习一下,比较简单,但是很基础很重要的。
关于线程间通信问题本篇到此就结束了,再说一次,多线程相关博客没什么新玩意,只是自己工作以来一次总结,虽然基础,枯燥,但是比较重要,希望本篇博客对您有用。
声明:文章将会陆续搬迁到个人公众号,以后文章也会第一时间发布到个人公众号,及时获取文章内容请关注公众号
java线程间通信:一个小Demo完全搞懂的更多相关文章
- Java多线程同步问题:一个小Demo完全搞懂
版权声明:本文出自汪磊的博客,转载请务必注明出处. Java线程系列文章只是自己知识的总结梳理,都是最基础的玩意,已经掌握熟练的可以绕过. 一.一个简单的Demo引发的血案 关于线程同步问题我们从一个 ...
- Java线程间通信-回调的实现方式
Java线程间通信-回调的实现方式 Java线程间通信是非常复杂的问题的.线程间通信问题本质上是如何将与线程相关的变量或者对象传递给别的线程,从而实现交互. 比如举一个简单例子,有一个多线程的 ...
- Java线程间通信之wait/notify
Java中的wait/notify/notifyAll可用来实现线程间通信,是Object类的方法,这三个方法都是native方法,是平台相关的,常用来实现生产者/消费者模式.我们来看下相关定义: w ...
- Java——线程间通信
body, table{font-family: 微软雅黑; font-size: 10pt} table{border-collapse: collapse; border: solid gray; ...
- 说说Java线程间通信
序言 正文 [一] Java线程间如何通信? 线程间通信的目标是使线程间能够互相发送信号,包括如下几种方式: 1.通过共享对象通信 线程间发送信号的一个简单方式是在共享对象的变量里设置信号值:线程A在 ...
- 说说 Java 线程间通信
序言 正文 一.Java线程间如何通信? 线程间通信的目标是使线程间能够互相发送信号,包括如下几种方式: 1.通过共享对象通信 线程间发送信号的一个简单方式是在共享对象的变量里设置信号值:线程A在一个 ...
- java线程间通信1--简单实例
线程通信 一.线程间通信的条件 1.两个以上的线程访问同一块内存 2.线程同步,关键字 synchronized 二.线程间通信主要涉及的方法 wait(); ----> 用于阻塞进程 noti ...
- java线程间通信之通过管道进行通信
管道流PipeStream是一种特殊的流,用于在不同线程间直接传送数据,而不需要借助临时文件之类的东西. jdk中提供了四个类来使线程间可以通信: 1)PipedInputStream和PipedOu ...
- Java 线程间通信 —— 等待 / 通知机制
本文部分摘自<Java 并发编程的艺术> volatile 和 synchronize 关键字 每个处于运行状态的线程,如果仅仅是孤立地运行,那么它产生的作用很小,如果多个线程能够相互配合 ...
随机推荐
- Kotlin——最详细的常量、变量、注释的使用
在Kotlin中的变量.常量以及注释多多少少和Java语言是有着不同之处的.不管是变量.常量的定义方式,还是注释的使用.下面详细的介绍Kotlin中的变量.常量.注释的使用.以及和Java的对比. 如 ...
- arcgis api for js之echarts开源js库实现地图统计图分析
前面写过一篇关于arcgis api for js实现地图统计图的,具体见:http://www.cnblogs.com/giserhome/p/6727593.html 那是基于dojo组件来实现图 ...
- 解决Windows和Linux使用npm打包js和css文件不同的问题
1.问题出现 最近公司上线前端H5页面,使用npm打包,特别奇怪的是每次打包发现css和js文件与Windows下打包不一致(网页使用Windows环境开发),导致前端页面功能不正常. 2.问题排查 ...
- Unable to make the module: related gradle configuration was not found. Please, re-import the Gradle project and try again
到stack overflow找到的答案,老外还是专业 I also had a similar problem, Go to : View -> Tool Windows -> Grad ...
- linux 操作中命令备忘
1 使用grep 查询关键内容 如果你想在当前目录下 查找"hello,world!"字符串,可以这样: grep -rn "hello,world!" * * ...
- Head First设计模式之生成器模式
一.定义 将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示.建造者模式是一种对象创建型模式. 二.结构 角色 Builder(抽象建造者):它为创建一个产品Product对象的 ...
- Android WebView存在跨域访问漏洞(CNVD-2017-36682)介绍及解决
Android WebView存在跨域访问漏洞(CNVD-2017-36682).攻击者利用该漏洞,可远程获取用户隐私数据(包括手机应用数据.照片.文档等敏感信息),还可窃取用户登录凭证,在受害者毫无 ...
- zxing .net 多种条码格式的生成
下载地址:http://zxingnet.codeplex.com/ zxing.net是.net平台下编解条形码和二维码的工具,使用非常方便. 本文主要说明一下多种类型条码的生成. 适用的场景,标签 ...
- div外观例子
title: div外观例子 date: 2018-1-15 14:00:00 tags: 前端 div css categories: 前端框架 --- 标题 我的标题 内容 Font Awesom ...
- Nginx 显示502 Bad Gateway错误
今天系统发布到测试环境,有一个用户登录后显示502 Bad Gateway错误,其他的用户都登录成功.但是在开发机链接测试数据库,使用该账号登录成功. 初步猜测是Nginx的问题,查看nginx的错 ...