Java多线程之生产者消费者问题<一>:使用synchronized keyword解决生产者消费者问题
今天看了一片博文,讲Java多线程之线程的协作,当中作者用程序实例说明了生产者和消费者问题,但我及其它读者发现程序多跑几次还是会出现死锁,百度搜了下大都数的样例也都存在bug,经过细致研究发现当中的问题。并攻克了,感觉有意义贴出来分享下。
以下首先贴出的是有bug的代码,一个4个类。Plate.java:
package CreatorAndConsumer; import java.util.ArrayList;
import java.util.List; /**
* 盘子,表示共享的资源
* @author Martin
*
*/
public class Plate {
private List<Object> eggs = new ArrayList<Object>(); /**
* 获取蛋
* @return
*/
public Object getEgg()
{
System.out.println("消费者取蛋");
Object egg = eggs.get(0);
eggs.remove(0);
return egg;
} /**
* 增加蛋
* @return
*/
public void addEgg(Object egg)
{
System.out.println("生产者生蛋");
eggs.add(egg);
} /**
* 获取蛋个数
* @return
*/
public int getEggNum()
{
return eggs.size();
}
}
消费者类:Consumer2.java
package CreatorAndConsumer; public class Consumer2 implements Runnable {
/**
* 线程资源
*/
private Plate plate; public Consumer2(Plate plate) {
this.plate = plate;
} @Override
public void run() {
synchronized (plate) {
// 假设此时蛋的个数大于0。则等等
while (plate.getEggNum() < 1) {
try {
// 这个细节须要注意,假设线程进入wait,那么其上的锁就会临时得到释放。
// 不然其它线程也不能进行加锁,然后唤醒本线程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 唤醒后。再次得到资源锁。且条件满足就能够放心地取蛋了
plate.getEgg();
plate.notify(); }
}
}
生产者类:Creator2.java
package CreatorAndConsumer; /**
* 生产者
*
* @author Martin
*
*/
public class Creator2 implements Runnable {
/**
* 线程资源
*/
private Plate plate; public Creator2(Plate plate) {
this.plate = plate;
} @Override
public void run() {
synchronized (plate) {
// 假设此时蛋的个数大于0,则等等
while (plate.getEggNum() >= 5) {
try {
// 这个细节须要注意,假设线程进入wait。那么其上的锁就会临时得到释放。
// 不然其它线程也不能进行加锁。然后唤醒本线程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 唤醒后,再次得到资源锁,且条件满足就能够放心地生蛋啦
Object egg = new Object();
plate.addEgg(egg);
plate.notify();
}
}
}
測试类:Tester.java
package CreatorAndConsumer; public class Tester {
public static void main(String[] args)
{
//共享资源
Plate plate = new Plate(); //加入生产者和消费者
for(int i = 0 ; i < 100; i ++)
{
//有bug版
new Thread(new Creator2(plate)).start();
new Thread(new Consumer2(plate)).start(); //无bug版
//new Thread(new Creator(plate)).start();
//new Thread(new Consumer(plate)).start();
}
}
}
假设多执行几次或者将測试类中的循环次数改大,则会发现出现死锁的概率还是非常高的。以下分析发生这样的问题的解决办法:
在jdk中对于Object.wait有这种一段解释:当前线程必须拥有此对象监视器。该线程放弃对此监视器的全部权并等待,直到其它线程通过调用 notify 方法,或 notifyAll 方法通知在此对象的监视器上等待的线程醒来。
可见以下一种情况就可能出现:消费者1进入等待状态(此时资源锁已被放开),假设此时消费者2获取到了资源同步锁(没有人保证消费者1进入等待,下一个拿到锁的一定是生产者)。消费者2推断没有资源也进入等待状态。此时生产者1生产了,并notify了消费者1,消费者1顺利地消费了,并运行notify操作,但此时消费者2却也由于资源而处于等待状态。从而唤醒了消费者2(消费者1本欲唤醒其它生产者)。而此时并没有不论什么资源。导致了整个程序由于消费者2陷入无限的等待。形成了死锁。
经过以上分析。究其根本原因是:同一时候几个消费者或几个生产者处于等待状态,导致消费者可能唤醒的还是消费者,或者生产者唤醒的还是生产者。那么假设我们可以保证同一时候仅仅有一个消费者处于wait状态(生产者同理),那就就能保证消费者唤醒的一定是生产者,从而能使整个任务顺利进行下去。以下是改动后的代码:
改进后的Consumer.java
package CreatorAndConsumer; public class Consumer implements Runnable {
/**
* 线程资源
*/
private Plate plate; /**
* 生产者锁:用于锁定同一时间仅仅能有一个生产者进入生产临界区(假设同一时候又两个生产者进入临界区。那么非常有可能当中一个生产者本想唤醒消费者却唤醒了生产者)
*/
private static Object consumerLocker = new Object(); public Consumer(Plate plate) {
this.plate = plate;
} @Override
public void run() {
// 必须先获得生产者锁才干生产
synchronized (consumerLocker) {
synchronized (plate) {
// 假设此时蛋的个数大于0,则等等
while (plate.getEggNum() < 1) {
try {
// 这个细节须要注意,假设线程进入wait,那么其上的锁就会临时得到释放。
// 不然其它线程也不能进行加锁,然后唤醒本线程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 唤醒后,再次得到资源锁,且条件满足就能够放心地取蛋了
plate.getEgg();
plate.notify(); }
}
}
}
改进后的Creator.java:
package CreatorAndConsumer; /**
* 生产者
*
* @author Martin
*
*/
public class Creator implements Runnable {
/**
* 线程资源
*/
private Plate plate; /**
* 生产者锁:用于锁定同一时间仅仅能有一个生产者进入生产临界区(假设同一时候又两个生产者进入临界区。那么非常有可能当中一个生产者本想唤醒消费者却唤醒了生产者)
*/
private static Object creatorLocker = new Object(); public Creator(Plate plate) {
this.plate = plate;
} @Override
public void run() {
//必须先获得生产者锁才干生产
synchronized (creatorLocker) {
synchronized (plate) {
// 假设此时蛋的个数大于0,则等等
while (plate.getEggNum() >= 5) {
try {
// 这个细节须要注意。假设线程进入wait,那么其上的锁就会临时得到释放。
// 不然其它线程也不能进行加锁,然后唤醒本线程
plate.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 唤醒后,再次得到资源锁,且条件满足就能够放心地生蛋啦
Object egg = new Object();
plate.addEgg(egg);
plate.notify();
}
}
}
}
改进说明:改进后的生产者和消费者分别加了生产者锁和消费者锁。分别用于锁定同一时间仅仅能有一个消费者(生产者)进入生产临界区(假设同一时候又两个生产者进入临界区,那么非常有可能当中一个生产者本想唤醒消费者却唤醒了生产者),总的来说就是一共三个锁:消费者锁、生产者锁、生产者和消费者共享的锁。
终于,写多线程的时候须要注意的是一个资源可能唤醒的是全部因该资源而等待的线程,因此消费者线程不一定唤醒的就是生产者线程也可能是消费者线程。
Java多线程之生产者消费者问题<一>:使用synchronized keyword解决生产者消费者问题的更多相关文章
- 浅谈Java多线程同步机制之同步块(方法)——synchronized
在多线程访问的时候,同一时刻只能有一个线程能够用 synchronized 修饰的方法或者代码块,解决了资源共享.下面代码示意三个窗口购5张火车票: package com.jikexueyuan.t ...
- Java多线程同步机制之同步块(方法)——synchronized
在多线程访问的时候,同一时刻只能有一个线程能够用 synchronized 修饰的方法或者代码块,解决了资源共享.下面代码示意三个窗口购5张火车票: package com.jikexueyuan.t ...
- java多线程中篇(二) —— 线程的创建和Synchronized锁关键字
学习之前,先了解线程状态图 说明:线程共包括以下5种状态. 1. 新建状态(New) : 线程对象被创建后,就进入了新建状态.例如,Thread thread = new Thread ...
- Java多线程基础知识(二)
一. Java线程具有6种状态 NEW 初始状态,线程被创建,但是还没有调用start方法. RUNNABLE 运行状态,java线程将操作系统中的就绪和运行两种状态笼统的称作进行中. BLOCKE ...
- [Java] 多线程基础详细总结,附加详细实例
详细代码在文章底部 目录 基础概念 进程与线程 单线程与多线程 实现线程的4中方式 thread.start()和runnable.run()的区别 Thread和Runnable的异同 线程的基本操 ...
- Java多线程-两种常用的线程计数器CountDownLatch和循环屏障CyclicBarrier
Java多线程编程-(1)-线程安全和锁Synchronized概念 Java多线程编程-(2)-可重入锁以及Synchronized的其他基本特性 Java多线程编程-(3)-从一个错误的双重校验锁 ...
- synchronized与static synchronized 的差别、synchronized在JVM底层的实现原理及Java多线程锁理解
本Blog分为例如以下部分: 第一部分:synchronized与static synchronized 的差别 第二部分:JVM底层又是怎样实现synchronized的 第三部分:Java多线程锁 ...
- Java多线程并发编程/锁的理解
一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等 ...
- Java 多线程与并发【原理第一部分笔记】
Java 多线程与并发[原理第一部分笔记] Synchronized synchronized的基本含义以及使用方式 在Java中线程安全问题的主要诱因就是存在共享数据(也称为临界资源)以及存在多条线 ...
随机推荐
- UVALive 5059 C - Playing With Stones 博弈论Sg函数
C - Playing With Stones Time Limit:3000MS Memory Limit:0KB 64bit IO Format:%lld & %llu S ...
- 单独配置secondarynamenode
一.配置说明 这是在我之前yarn框架上通过加入节点,改动相关的配置文件,使得secondarynamenode独立出来的,所以这里前期的一系列琐碎配置请參考我之前的博客: http://blog.c ...
- pytest文档16-用例a失败,跳过测试用例b和c并标记失败xfail
前言 当用例a失败的时候,如果用例b和用例c都是依赖于第一个用例的结果,那可以直接跳过用例b和c的测试,直接给他标记失败xfail 用到的场景,登录是第一个用例,登录之后的操作b是第二个用例,登录之后 ...
- [ IOS ] 视图控制对象ViewController的生命周期
init-初始化程序 viewDidLoad-加载视图 viewWillAppear-UIViewController对象的视图即将加入窗口时调用: viewDidApper-UIViewContro ...
- 内存控制篇calloc free getpagesize malloc mmap munmap
calloc(配置内存空间) 相关函数 malloc,free,realloc,brk 表头文件 #include <stdlib.h> 定义函数 void *calloc(size_t ...
- Top N之MapReduce程序加强版Enhanced MapReduce for Top N items
In the last post we saw how to write a MapReduce program for finding the top-n items of a dataset. T ...
- 编译打包工具sbt的镜像设置
sbt可以和maven共用一个镜像,公司内部有的自然最后不过 创建文件:~/.sbt/repositories [repositories] local aliyun: http://maven.al ...
- 在Ubuntu 12.04 桌面上设置启动器(快捷方式)
在Ubuntu 12.04 桌面上设置启动器(快捷方式)过程讲解: 如下图所示,Eclipse 和 SQLDeveloper 都可以直接双击打开,这些应用程序的启动器都在 /usr/share/app ...
- [7] 金字塔(Pyramid)图形的生成算法
顶点数据的生成 bool YfBuildPyramidVertices ( Yreal width, Yreal length, Yreal height, YeOriginPose originPo ...
- C语言 数组初始化的三种常用方法({0}, memset, for循环赋值)以及原理
C语言中,数组初始化的方式主要有三种: 1.声明时,使用 {0} 初始化: 2.使用memset: 3.用for循环赋值. 那么,这三种方法的原理以及效率如何呢? 请看下面的测试代码: #define ...