Java 多线程详解(四)------生产者和消费者
通过前面三篇博客的介绍,基本上对Java的多线程有了一定的了解了,然后这篇博客根据生产者和消费者的模型来介绍Java多线程的一些其他知识。
我们这里的生产者和消费者模型为:
生产者Producer 生产某个对象(共享资源),放在缓冲池中,然后消费者从缓冲池中取出这个对象。也就是生产者生产一个,消费者取出一个。这样进行循环。
第一步:我们先创建共享资源的类 Person,它有两个方法,一个生产对象,一个消费对象
public class Person {
private String name;
private int age;
/**
* 生产数据
* @param name
* @param age
*/
public void push(String name,int age){
this.name = name;
this.age = age;
}
/**
* 取数据,消费数据
* @return
*/
public void pop(){
System.out.println(this.name+"---"+this.age);
}
}
第二步:创建生产者线程,并在 run() 方法中生产50个对象
public class Producer implements Runnable{
//共享资源对象
Person p = null;
public Producer(Person p){
this.p = p;
}
@Override
public void run() {
//生产对象
for(int i = 0 ; i < 50 ; i++){
//如果是偶数,那么生产对象 Tom--11;如果是奇数,则生产对象 Marry--21
if(i%2==0){
p.push("Tom", 11);
}else{
p.push("Marry", 21);
}
}
}
}
第三步:创建消费者线程,并在 run() 方法中消费50个对象
public class Consumer implements Runnable{
//共享资源对象
Person p = null;
public Consumer(Person p) {
this.p = p;
}
@Override
public void run() {
for(int i = 0 ; i < 50 ; i++){
//消费对象
p.pop();
}
}
}
由于我们的模型是生产一个,马上消费一个,那期望的结果便是 Tom---11,Marry--21,Tom---11,Mary---21...... 连续这样交替出现50次
但是结果却是:
Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
......
Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
为了让结果产生的更加明显,我们在共享资源的 pop() 和 push() 方法中添加一段延时代码
/**
* 生产数据
* @param name
* @param age
*/
public void push(String name,int age){
this.name = name;
try {
//这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.age = age;
}
/**
* 取数据,消费数据
* @return
*/
public void pop(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name+"---"+this.age);
}
这个时候,结果如下:
Marry---11
Tom---21
Marry---11
Tom---21
Marry---11
Tom---21
Marry---11
Tom---21
......
Tom---11
Tom---21
Marry---11
Tom---21
Marry---11
Marry---21
结果分析:这时候我们发现结果全乱套了,Marry--21是固定的,Tom--11是固定的,但是上面的结果全部乱了,那这又是为什么呢?而且有很多重复的数据连续出现,那这又是为什么呢?
原因1:出现错乱数据,是因为先生产出Tom--11,但是消费者没有消费,然后生产者继续生产出name为Marry,但是age还没有生产,而消费者这个时候拿去消费了,那么便出现 Marry--11。同理也会出现 Tom--21
原因2:出现重复数据,是因为生产者生产一份数据了,消费者拿去消费了,但是第二次生产者生产数据了,但是消费者没有去消费;而第三次生产者继续生产数据,消费者才开始消费,这便会产生重复
解决办法1:生产者生产name和age必须要是一个整体一起完成,即同步。生产的中间不能让消费者来消费即可。便不会产生错乱的数据。如何同步可以参考:
Java 多线程详解(三)------线程的同步:http://www.cnblogs.com/ysocean/p/6883729.html
这里我们选择同步方法(在方法前面加上 synchronized)
public class Person {
private String name;
private int age;
/**
* 生产数据
* @param name
* @param age
*/
public synchronized void push(String name,int age){
this.name = name;
try {
//这段延时代码的作用是可能只生产了 name,age为nul,消费者就拿去消费了
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.age = age;
}
/**
* 取数据,消费数据
* @return
*/
public synchronized void pop(){
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(this.name+"---"+this.age);
}
}
结果如下:
Marry---21
Marry---21
Marry---21
Marry---21
Marry---21
Tom---11
Tom---11
......
Tom---11
Tom---11
Tom---11
Tom---11
Tom---11
问题:还是没有解决上面的问题2,出现重复的问题。期望的结果是 Tom---11,Marry--21,Tom---11,Mary---21...... 连续这样交替出现50次。那如何解决呢?
解决办法:生产者生产一次数据了,就暂停生产者线程,等待消费者消费;消费者消费完了,消费者线程暂停,等待生产者生产数据,这样来进行。
这里我们介绍一个同步锁池的概念:
同步锁池:同步锁必须选择多个线程共同的资源对象,而一个线程获得锁的时候,别的线程都在同步锁池等待获取锁;当那个线程释放同步锁了,其他线程便开始由CPU调度分配锁
关于让线程等待和唤醒线程的方法,如下:(这是 Object 类中的方法)


wait():执行该方法的线程对象,释放同步锁,JVM会把该线程放到等待池中,等待其他线程唤醒该线程
notify():执行该方法的线程唤醒在等待池中等待的任意一个线程,把线程转到锁池中等待(注意锁池和等待池的区别)
notifyAll():执行该方法的线程唤醒在等待池中等待的所有线程,把线程转到锁池中等待。
注意:上述方法只能被同步监听锁对象来调用,这也是为啥wait() 和 notify()方法都在 Object 对象中,因为同步监听锁可以是任意对象,只不过必须是需要同步线程的共同对象即可,否则别的对象调用会报错: java.lang.IllegalMonitorStateException
假设 A 线程和 B 线程同时操作一个 X 对象,A,B 线程可以通过 X 对象的 wait() 和 notify() 方法来进行通信,流程如下:
①、当线程 A 执行 X 对象的同步方法时,A 线程持有 X 对象的 锁,B线程在 X 对象的锁池中等待
②、A线程在同步方法中执行 X.wait() 方法时,A线程释放 X 对象的锁,进入 X 对象的等待池中
③、在 X 对象的锁池中等待锁的 B 线程获得 X 对象的锁,执行 X 的另一个同步方法
④、B 线程在同步方法中执行 X.notify() 方法,JVM 把 A 线程从等待池中移动到 X 对象的锁池中,等待获取锁
⑤、B 线程执行完同步方法,释放锁,等待获取锁的 A 线程获得锁,继续执行同步方法
那么为了解决上面重复的问题,修改代码如下:
public class Person {
private String name;
private int age;
//表示共享资源对象是否为空,如果为 true,表示需要生产,如果为 false,则有数据了,不要生产
private boolean isEmpty = true;
/**
* 生产数据
* @param name
* @param age
*/
public synchronized void push(String name,int age){
try {
//不能用 if,因为可能有多个线程
while(!isEmpty){//进入到while语句内,说明 isEmpty==false,那么表示有数据了,不能生产,必须要等待消费者消费
this.wait();//导致当前线程等待,进入等待池中,只能被其他线程唤醒
}
//-------生产数据开始-------
this.name = name;
//延时代码
Thread.sleep(10);
this.age = age;
//-------生产数据结束-------
isEmpty = false;//设置 isEmpty 为 false,表示已经有数据了
this.notifyAll();//生产完毕,唤醒所有消费者
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 取数据,消费数据
* @return
*/
public synchronized void pop(){
try {
//不能用 if,因为可能有多个线程
while(isEmpty){//进入 while 代码块,表示 isEmpty==true,表示为空,等待生产者生产数据,消费者要进入等待池中
this.wait();//消费者线程等待
}
//-------消费开始-------
Thread.sleep(10);
System.out.println(this.name+"---"+this.age);
//-------消费结束------
isEmpty = true;//设置 isEmpty为true,表示需要生产者生产对象
this.notifyAll();//消费完毕,唤醒所有生产者
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
结果:
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
......
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
Tom---11
Marry---21
那么这便是我们期待的结果,交替出现。
死锁:
①、多线程通信的时候,很容易造成死锁,死锁无法解决,只能避免
②、当 A 线程等待由 B 线程持有的锁,而 B 线程正在等待由 A 线程持有的锁时发生死锁现象(比如A拿着铅笔,B拿着圆珠笔,A说你先给我圆珠笔,我就把铅笔给你,而B说你先给我铅笔,我就把圆珠笔给你,这就造成了死锁,A和B永远不能进行交换)
③、JVM 既不检测也不避免这种现象,所以程序员必须保证不能出现这样的情况
Thread 类中容易造成死锁的方法(这两个方法都已经过时了,不建议使用):
suspend():使正在运行的线程放弃 CPU,暂停运行(不释放锁)
resume():使暂停的线程恢复运行
情景:A 线程获得对象锁,正在执行一个同步方法,如果 B线程调用 A 线程的 suspend() 方法,此时A 暂停运行,放弃 CPU 资源,但是不放弃同步锁,那么B也不能获得锁,A又暂停,那么便造成死锁。
解决死锁法则:当多个线程需要访问 共同的资源A,B,C时,必须保证每一个线程按照一定的顺序去访问,比如都先访问A,然后B,最后C。就像我们这里的生产者---消费者模型,制定了必须生产者先生产一个对象,然后消费者去消费,消费完毕,生产者才能在开始生产,然后消费者在消费。这样的顺序便不会造成死锁。
Java 多线程详解(四)------生产者和消费者的更多相关文章
- Java多线程详解
Java线程:概念与原理 一.操作系统中线程和进程的概念 现在的操作系统是多任务操作系统.多线程是实现多任务的一种方式. 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间,一个进程 ...
- java 多线程详解
一.重点 重点: 1.创建和启动线程 2.实现线程调度 3.实现线程同步 4.实现线程通信 1.为什么要学习多线程? 当多个人访问电脑上同一资源的时候,要用到多线程,让每个人感觉很多电脑同时为多个人服 ...
- Java多线程详解——一篇文章搞懂Java多线程
目录 1. 基本概念 2. 线程的创建和启动 2.1. 多线程实现的原理 2.2.多线程的创建,方式一:继承于Thread类 2.3.多线程的创建,方式一:创建Thread匿名子类(也属于方法一) 2 ...
- Java多线程详解(二)
评论区留下邮箱可获得<Java多线程设计模式详解> 转载请指明来源 1)后台线程 后台线程是为其他线程服务的一种线程,像JVM的垃圾回收线程就是一种后台线程.后台线程总是等到非后台线程死亡 ...
- java多线程详解(6)-线程间的通信wait及notify方法
Java多线程间的通信 本文提纲 一. 线程的几种状态 二. 线程间的相互作用 三.实例代码分析 一. 线程的几种状态 线程有四种状态,任何一个线程肯定处于这四种状态中的一种:(1). 产生(New) ...
- Java多线程详解(三)
1)死锁 两个线程相互等待对方释放同步监视器时会出现死锁的现象,这时所有的线程都处于阻塞状态,程序无法继续向下执行. 如下就是会出现死锁的程序. 首先flag = 1,线程d1开始执行,锁住对象o1, ...
- Java 多线程详解(一)------概念的引入
这是讲解 Java 多线程的第一章,我们在进入讲解之前,需要对以下几个概念有所了解. 1.并发和并行 并行:指两个或多个时间在同一时刻发生(同时发生): 并发:指两个或多个事件在一个时间段内发生. 在 ...
- 原创Java多线程详解(一)
只看书不实践是不行的.来实践一下~~~~~~(引用请指明来源) 先看看百科对多线程的介绍 多线程(英语:multithreading),是指从软件或者硬件上实现多个线程并发执行的技术.具有多线程能力的 ...
- Java多线程详解总结
一.基本概念 程序(program): 是为完成特定任务.用某种语言编写的一组指令的集合.即指一 段静态的代码,静态对象. 进程(process):是程序的一次执行过程,或是正在运行的一个程序.是一个 ...
随机推荐
- body全屏
html, body { min-height: 100%; }
- android开发之 包含EditText组件 禁止自动获取焦点弹出输入法
在EditText标签的外层Layout中加入focusableInTouchMode属性 android:focusableInTouchMode="true" 即可.
- Fragment防止自动清理 (ViewPager滑动时,滑出屏幕后被清理)(转)
原文链接:http://www.xuebuyuan.com/2231000.html 这个问题网上搜一搜发现帖子很多,但是博主试了几种好像没有说的那么好用 一. 比如给ViewPager设置长度,以增 ...
- apache的配置参数
#ErrorDocument 500 "The server made a boo boo."#ErrorDocument 404 /missing.html 1.Document ...
- activiti 一个流程的运转步骤 以请假流程为例
---为了加深对activiti的理解记忆,对自己做的一个流程进行自述.加强记忆 请假实例 一.设计请假的流程图以及流程文件,完善对应数据项,比如用户信息,请假单信息 --请假单 --流程图 --流程 ...
- 处理json数据的空数据为任意字符
处理json数据的空数据为任意字符 有时候从后台返回来的数据需要处理一下,根据实际开发需求,不能在页面上直接显示空字符,需要显示为"无内容"或者其他字段,而有些json数据结构比较 ...
- 使用swagger实现web api在线接口文档
一.前言 通常我们的项目会包含许多对外的接口,这些接口都需要文档化,标准的接口描述文档需要描述接口的地址.参数.返回值.备注等等:像我们以前的做法是写在word/excel,通常是按模块划分,例如一个 ...
- js:通过正则匹配获取页面的url中的参数
简介:获取页面参数 原生js: //通过正则匹配获取当前页面的url中的参数 function getUrlParam(name){ var reg = new RegExp("(^|&am ...
- php 启动过程 - sapi MSHUTDOWN 过程
php 启动过程 - sapi MSHUTDOWN 过程 概述 当服务器关闭时, 会走到 sapi MSHUTDOWN 过程 注册过程 本次内容是在 php 启动过程 - sapi MINIT 过程 ...
- 【stm32】时钟树解析
有时候会突然忘了这个重要的时钟树,这里转载一个比较好的,以防忘记. STM32时钟系统 在STM32中,有五个时钟源,为HSI.HSE.LSI.LSE.PLL. ①HSI是高速内部时钟,RC振荡器,频 ...