生产者-消费者问题【Java实现】
生产者-消费者问题是经典的并发问题, 非常适合并发入门的编程练习。
生产者-消费者问题是指, 有若干个生产者和若干个消费者并发地读写一个或多个共享存储空间;生产者创建对象并放入到共享存储空间,消费者从共享存储空间取出对象进行消费处理。当共享存储空间为满时,生产者被阻塞;当共享存储空间为空时,消费者被阻塞。本文先使用一个自定义的有限长度字符序列缓冲区来作为共享存储空间,并使用原生的 wait 和 notify 机制来实现并发读写; 接着使用 Java 并发库提供的 BlockQueue 来实现同样的目的。
一、 使用自定义的有限长度字符序列缓冲区来作为共享存储空间:
(1) 该缓冲区必须是线程安全的, 比较简单可行的是在方法上加入 synchronized 关键字;
(2) 在并发读写该缓冲区的线程 Producer 和 Comsumer 中,要小心地将 wait 方法放在 while 循环中, 使用 notifyAll 来通知;
(3) 即使缓冲区是线程安全的,要确保操作及其展示结果一致时需要使用同步确保一起执行;
(4) 使用了可见性变量 volatile boolean endflag 来取消线程的执行, 更好的方式是通过线程中断。
/**
* PCProblem :
* 模拟生产者-消费者问题, 生产者产生字符并写入字符序列缓冲区, 消费者从缓冲区取走字符
*
* @author shuqin1984 2011-08-05
*
*/ package threadprogramming.basic.simulation; import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit; public class PCProblem { public static void main(String[] args) { System.out.println(" ---- Thread main starts up ---- "); // 模拟 生产者 - 消费者 任务 SharedCharBuffer sharedBuffer = new SharedCharBuffer(10);
ExecutorService es = Executors.newCachedThreadPool(); for (int i=1; i <= 10; i++) {
es.execute(new ProducerThread(i, sharedBuffer));
es.execute(new ConsumerThread(i, sharedBuffer));
}
es.shutdown(); // 运行 5 秒后终止模拟 try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
} ProducerThread.cancel();
ConsumerThread.cancel();
es.shutdownNow(); System.out.println("Time to be over."); } }
生产者: Producer.java
/**
* ProducerThread: 生产者线程
*/ package threadprogramming.basic.simulation; import java.util.Random;
import java.util.concurrent.TimeUnit; public class ProducerThread extends Thread { private static String str = "abc1defg2hijk3lmno4pqrs5tuvwx6yz" +
"AB7CDEF8GHIJK9LMNO0PQR_STU*VWXYZ"; private static volatile boolean endflag = false; private final int id; private SharedCharBuffer buffer; public ProducerThread(int id, SharedCharBuffer buffer) {
this.id = id;
this.buffer = buffer;
} public static void cancel() {
endflag = true;
} public boolean isCanceled() {
return endflag == true;
} /**
* 生产者任务: 只要任务不取消,且缓冲区不满,就往缓冲区中字符
*/
public void run()
{
while (!isCanceled() && !Thread.interrupted()) {
synchronized (buffer) {
while (buffer.isFull()) {
// 缓冲区已满,生产者必须等待
try {
buffer.wait();
} catch (InterruptedException e) {
System.out.println(this + " Interrupted.");
}
}
char ch = produce();
System.out.println(TimeIndicator.getcurrTime() + ":\t" + this + " 准备写缓冲区:" + ch);
buffer.write(ch);
System.out.println(TimeIndicator.getcurrTime() + ":\t" + this + " :\t\t\t" + buffer);
buffer.notifyAll();
}
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println(this + " Interrupted.");
}
}
System.out.println("Exit from: " + this);
} public char produce()
{
Random rand = new Random();
return str.charAt(rand.nextInt(64));
} public String toString()
{
return "P[" + id + "]";
} }
消费者:
/**
* ConsumerThread: 消费者线程
*
*/ package threadprogramming.basic.simulation; import java.util.concurrent.TimeUnit; public class ConsumerThread implements Runnable { private static volatile boolean endflag = false; private final int id; private SharedCharBuffer buffer; public ConsumerThread(int id, SharedCharBuffer buffer) {
this.id = id;
this.buffer = buffer;
} public static void cancel() {
endflag = true;
} public boolean isCanceled() {
return endflag == true;
} /**
* consume:
* 当缓冲区buffer中有字符时,就取出字符显示【相当于消费者】。
*
*/
public char consume() {
return buffer.fetch();
} /**
* 消费者任务: 只要任务不取消,且缓冲区不被置空,就从缓冲区中取出字符消费。
*/
public void run() { while (!isCanceled() && !Thread.interrupted()) {
synchronized (buffer) {
while (buffer.isEmpty()) {
try {
buffer.wait();
} catch (InterruptedException e) {
System.out.println(this + " Interrupted.");
}
}
System.out.println(TimeIndicator.getcurrTime() + ":\t" + this + " 取出字符: " + consume());
System.out.println(TimeIndicator.getcurrTime() + ":\t" + this + " :\t\t\t" + buffer);
buffer.notifyAll();
}
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
System.out.println(this + " Interrupted.");
}
}
System.out.println("Exit from: " + this); } public String toString() {
return "C[" + id + "]";
} }
有限字符缓冲区: SharedCharBuffer.java
/**
* CharBuffer:
* 实现有限长度字符缓冲区的互斥读写。
*
*/ package zzz.study.threadprogramming.basic.simulation; public class CharBuffer { private final int capacity; // 指定字符缓冲区能容纳的字符数 private char[] charBuffer; // 用来生产和消费的有限长度字符缓冲区
private int index; private int count; // 该缓冲区被读写的次数,可衡量性能 public CharBuffer(int capacity) { if (charBuffer == null) {
charBuffer = new char[capacity];
}
this.capacity = capacity;
index = 0; } /**
* 判断缓冲区是否已满,满则生产者等待
*/
public boolean isFull()
{
return index == capacity;
} /**
* 判断缓冲区是否为空,空则消费者等待
*/
public boolean isEmpty()
{
return index == 0;
} /**
* write: 将给定字符写入缓冲区中【改变了缓冲区内容】
* synchronized 关键字用于实现互斥访问缓冲区
* @param ch character that will be written into the buffer.
*
*/
public synchronized void write(char ch) { charBuffer[index] = ch;
index++;
count++;
} /**
* read: 读取缓冲区中给定位置的字符【不改变缓冲区内容】
* synchronized 关键字用于实现互斥访问缓冲区
* @param index integer representation of the position
*
*/
public synchronized char read(int index) {
return charBuffer[index];
} /**
* fetch: 取出缓冲区给定位置的字符【改变了缓冲区内容】
* synchronized 关键字用于实现互斥访问缓冲区
*
*/
public synchronized char fetch() { index--;
count++;
return charBuffer[index];
} /**
* getStringOfBuffer: 缓冲区内容的字符串表示
* @return string representation of the buffer's contents
*
*/
public synchronized String toString() { if (isEmpty()) {
return "缓冲区为空!";
}
else {
StringBuilder bufferstr = new StringBuilder("缓冲区内容: ");
for (int i=0; i < index; i++) {
bufferstr.append(charBuffer[i]);
}
return bufferstr.toString();
} } public int getCount() {
return count;
} }
二、 使用阻塞队列 BlockQueue 来实现生产者-消费者问题求解
可以看到, 客户端代码简化了不少, 错误风险也降低了。 只要在主线程创建一个 BlockQueue, 传给生产者 ProducerUsingQueue 和 消费者 ConsumerUsingQueue , 然后直接使用 BlockQueue 提供的同步机制。BlockQueue 在内部分别使用了Condition notFull 和 notEmpty 分别来通知 生产者和消费者, 在方法实现中使用了可重入锁 ReentrantLock 来确保并发互斥的操作。
package zzz.study.threadprogramming.basic.simulation.usequeue; import org.apache.commons.logging.Log;
import org.apache.log4j.Logger;
import zzz.study.threadprogramming.basic.simulation.TimeIndicator; import java.util.Random;
import java.util.concurrent.BlockingQueue; public class ProducerUsingQueue extends Thread { private static String str = "abc1defg2hijk3lmno4pqrs5tuvwx6yz" +
"AB7CDEF8GHIJK9LMNO0PQR_STU*VWXYZ"; private static volatile boolean endflag = false; private final int id; BlockingQueue<Character> buffer; private Logger log = Logger.getLogger("appInfo"); public ProducerUsingQueue(int id, BlockingQueue<Character> buffer) {
this.id = id;
this.buffer = buffer;
} public static void cancel() {
endflag = true;
} public boolean isCanceled() {
return endflag == true;
} public void run()
{
while (!isCanceled()) {
try {
char ch = produce();
log.info(TimeIndicator.getcurrTime() + ":\t" + this + " 准备写缓冲区:" + ch);
buffer.put(ch);
log.info(TimeIndicator.getcurrTime() + ":\t" + this + " :\t\t\t" + buffer);
} catch (InterruptedException e) {
log.error(this + " Interrupted: " + e.getMessage());
}
}
} public char produce()
{
Random rand = new Random();
return str.charAt(rand.nextInt(64));
} public String toString()
{
return "P[" + id + "]";
} }
/**
* CharOutputThread:
* 通过创建线程,并使用CharBuffer来实现并发地读和写字符缓冲区的仿真
*
*/ package zzz.study.threadprogramming.basic.simulation.usequeue; import org.apache.log4j.Logger;
import zzz.study.threadprogramming.basic.simulation.TimeIndicator; import java.util.concurrent.BlockingQueue; public class ConsumerUsingQueue extends Thread { private static volatile boolean endflag = false; private final int id; private BlockingQueue<Character> buffer; private Logger log = Logger.getLogger("appInfo"); public ConsumerUsingQueue(int id, BlockingQueue<Character> buffer) {
this.id = id;
this.buffer = buffer;
} public static void cancel() {
endflag = true;
} public boolean isCanceled() {
return endflag == true;
} /**
* consume:
* 当缓冲区buffer中有字符时,就取出字符显示【相当于消费者】。
*
*/
public Character consume() throws InterruptedException {
return buffer.take(); } /**
* run:
* 一个被创建的任务,只要缓冲区不被置空,就从缓冲区中取出字符消费。
*/
public void run() { while (!isCanceled()) {
try {
log.info(TimeIndicator.getcurrTime() + ":\t" + this + " 取出字符: " + consume());
log.info(TimeIndicator.getcurrTime() + ":\t" + this + " :\t\t\t" + buffer);
} catch (InterruptedException e) {
log.error(this + " Interrupted: " + e.getMessage());
}
}
} public String toString() {
return "C[" + id + "]";
}
}
/**
* TestThread :
*
* 使用主线程不断从键盘缓冲区获取输入,写入自创建的字符缓冲区,并显示缓冲区内容;
* 使用一个子线程不断从自创建的字符缓冲区取出字符输出,并显示缓冲区内容;
*
*/ package zzz.study.threadprogramming.basic.simulation.usequeue; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*; public class ProducerConsumerProblem { public static void main(String[] args) { int num = 10; System.out.println(" ---- Thread main starts up ---- "); BlockingQueue<Character> queue = new ArrayBlockingQueue<Character>(15);
ExecutorService es = Executors.newCachedThreadPool(); List<ProducerUsingQueue> producers = new ArrayList<ProducerUsingQueue>();
List<ConsumerUsingQueue> comsumers = new ArrayList<ConsumerUsingQueue>(); for (int i=0; i < num; i++) {
producers.add(new ProducerUsingQueue(i, queue));
comsumers.add(new ConsumerUsingQueue(i, queue));
} for (int i=0; i < num; i++) {
es.execute(producers.get(i));
es.execute(comsumers.get(i));
}
es.shutdown();
try {
TimeUnit.SECONDS.sleep(15);
} catch (InterruptedException e) {
e.printStackTrace();
}
ProducerUsingQueue.cancel();
ConsumerUsingQueue.cancel();
es.shutdownNow(); System.out.println("Time to be over."); } }
这里使用了 log4j 来打印日志,相关配置如下:
pom.xml 加入以下依赖:
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
log4j.properties 配置:
log4j.rootLogger = INFO,stdout
log4j.logger.appInfo = INFO,appInfoBrefLog log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ISO8601} %p [%c] - [%m]%n log4j.appender.appInfoBrefLog=org.apache.log4j.DailyRollingFileAppender
log4j.appender.appInfoBrefLog.File=./app/info_bref.log
log4j.appender.appInfoBrefLog.DatePattern ='.'yyyy-MM-dd
log4j.appender.appInfoBrefLog.Threshold=INFO
log4j.appender.appInfoBrefLog.Append=true
log4j.appender.appInfoBrefLog.layout=org.apache.log4j.PatternLayout
log4j.appender.appInfoBrefLog.layout.ConversionPattern=%r %m%n
生产者-消费者问题【Java实现】的更多相关文章
- 生产者消费者问题Java三种实现
生产者-消费者Java实现 2017-07-27 1 概述 生产者消费者问题是多线程的一个经典问题,它描述是有一块缓冲区作为仓库,生产者可以将产品放入仓库,消费者则可以从仓库中取走产品. 解决生产者/ ...
- 生产者消费者模式-Java实现
感知阶段 随着软件业的发展,互联网用户的日渐增多,并发这门艺术的兴起似乎是那么合情合理.每日PV十多亿的淘宝,处理并发的手段可谓是业界一流.用户访问淘宝首页的平均等待时间只有区区几秒,但是服务器所处理 ...
- 生产者消费者模型Java实现
生产者消费者模型 生产者消费者模型可以描述为: ①生产者持续生产,直到仓库放满产品,则停止生产进入等待状态:仓库不满后继续生产: ②消费者持续消费,直到仓库空,则停止消费进入等待状态:仓库不空后,继续 ...
- 生产者消费者模型java
马士兵老师的生产者消费者模型,我感觉理解了生产者消费者模型,基本懂了一半多线程. public class ProducerConsumer { public static void main(Str ...
- 生产者消费者模型-Java代码实现
什么是生产者-消费者模式 比如有两个进程A和B,它们共享一个固定大小的缓冲区,A进程产生数据放入缓冲区,B进程从缓冲区中取出数据进行计算,那么这里其实就是一个生产者和消费者的模式,A相当于生产者,B相 ...
- 生产者消费者的java实现
先看最简单的,也就是缓冲区的容量为1 缓冲区容量为1 import java.util.List; public class ProducerAndConsumer2 { static class A ...
- JAVA多线程之生产者 消费者模式 妈妈做面包案例
创建四个类 1.面包类 锅里只可以放10个面包 ---装面包的容器2.厨房 kitchen 生产面包 和消费面包 最多生产100个面包3.生产者4消费者5.测试类 多线程经典案例 import ja ...
- 第23章 java线程通信——生产者/消费者模型案例
第23章 java线程通信--生产者/消费者模型案例 1.案例: package com.rocco; /** * 生产者消费者问题,涉及到几个类 * 第一,这个问题本身就是一个类,即主类 * 第二, ...
- Java里的生产者-消费者模型(Producer and Consumer Pattern in Java)
生产者-消费者模型是多线程问题里面的经典问题,也是面试的常见问题.有如下几个常见的实现方法: 1. wait()/notify() 2. lock & condition 3. Blockin ...
- Java数据结构之队列的实现以及队列的应用之----简单生产者消费者应用
Java数据结构之---Queue队列 队列(简称作队,Queue)也是一种特殊的线性表,队列的数据元素以及数据元素间的逻辑关系和线性表完全相同,其差别是线性表允许在任意位置插入和删除,而队列只允许在 ...
随机推荐
- web页面实现指定区域打印功能
web页面实现指定区域打印功能 使用CSS,定义一个.noprint的class,将不打印的内容放入这个class内. 详细如下: <style media=print type="t ...
- jquery暂停和中断循环
jquery对数组进行循环,如果要求每次循环的时候暂停2秒钟,在.earch循环的时候,无论怎么设置,都不会暂停. setTimeout也只是在第一次执行的时候暂停. 原因猜测: js开始执行多线程? ...
- 使用spring提供的LocalSessionFactoryBean来得到SessionFactory
一直不明白,spring为什么可以通过注入LocalSessionFactoryBean的bean名称来得到SessionFactory,起初以为LocalSessionFactoryBean必然是S ...
- # 20145334 《Java程序设计》第9周学习总结
20145334 <Java程序设计>第9周学习总结 教材学习内容总结 第十六章 整合数据库 JDBC 1.Java语言访问数据库的一种规范,是一套API. 2.JDBC (Java Da ...
- Color Space: Ycc
在进行图像扫描时,有一种重要的扫描输入设备PhotoCd,由于PhotoCd在存储图像的时候要经过一种模式压缩,所以PhotoCd采用了Ycc颜色空间,此空间将亮度作由它的主要组件,具有两个单独的颜色 ...
- Codeforces Round #361 (Div. 2) C D
C 给出一个m 此时有 四个数 分别为x k*x k*k*x k*k*k*x k大于1 x大于等于1 要求求出来一个最小的值n 使其满足 这四个数中的最大值小于n 这四个数可能的组数为m 可以看出这四 ...
- LightOj 1197 - Help Hanzo(分段筛选法 求区间素数个数)
题目链接:http://lightoj.com/volume_showproblem.php?problem=1197 题意:给你两个数 a b,求区间 [a, b]内素数的个数, a and b ( ...
- Git merge 与 git rebase的区别
Git merge的用法: git merge Dev // Dev表示某分支,表示在当前分支合并Dev分支 git merge -m "Merge from Dev" Dev ...
- Eclipse安装maven插件报错
Eclipse安装maven插件,报错信息如下: Cannot complete the install because one or more required items could not be ...
- windows在cmd执行svn 命令
1. 使用svn 命令行工具. 找到http://www.visualsvn.com/downloads/ 下载Apache Subversion command line tools,这是一个可以在 ...