生产者-消费者模式指的是:生产者和消费者在同一个时间段共用同一段空间,在这段时间内,生产者负责往存储空间生产数据,而消费者则负责消费数据。实际上存在很多类似的场景:消息中间件就可以看做这种模式的应用,客户端请求负责把请求消息发送给消息中间件,然后由服务器负责从消息中间件获取请求并进行响应;还有网络编程中Socket也可以看做是生产者消费者模式的应用,etc。
为了简化问题的研究,现在仅仅考虑一种简单的场景:生产者负责生产“authorA–>titleA”和“authorB–>titleB”两种数据(这两种数据是一种代表author和title可以认为是生产的两种组件,并无实际意义),消费者则负责从中取数据。根据前面对线程安全的研究,该场景可能会出现取到的数据不完整或者重复取数据的情况。出现数据完整表现在:生产者生产完author后,还没有来得及生产title便被消费者取走了,这种情况需要使用线程同步机制实现;取到重复的数据表现在:消费者取到完整的数据后,没有及时通知生产者继续生产,导致第二次取到的数据仍然是上次取到的数据,显然这种情况需要使用等待/通知机制实现。
首先我们来定义需要生产的组件:Content。Content包括了两个组件信息:author和title,并提供了消费者和生产者执行的get和set方法
package com.ddkk.concurrency;
import java.util.concurrent.TimeUnit;
/**
* DDKK.COM 弟弟快看,程序员编程资料站 16-4-4.
*/
public class Content {
//标题
private String title;
//作者
private String author;
//是否开始生产的标志,默认开始生产
private boolean produce = true;
/**
* 消费者执行的方法
* @param author
* @param title
* @throws InterruptedException
*/
public synchronized void set(String author, String title) throws InterruptedException {
//如果没有开始生产就阻塞等待
while (!produce) {
super.wait();
}
//设置作者
this.setAuthor(author);
//休眠1秒
TimeUnit.SECONDS.sleep(1);
//设置标题
this.setTitle(title);
System.out.println("[生产者]:" + this.getAuthor() + " --> " + this.getTitle());
//设置标志位为false。表示可以取数据了
produce = false;
//唤醒正在等待的线程
super.notify();
}
/**
* 消费者执行的方法
* @throws InterruptedException
*/
public synchronized void get() throws InterruptedException {
//如果已经开始生产了就阻塞等待
while (produce){
super.wait();
}
System.out.println("[消费者]:" + this.getAuthor() + " --> " + this.getTitle());
//让生产者继续生产
produce = true;
super.notify();
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
然后,我们定义了生产者,把生产者执行的生产任务放在一个线程中单独执行:
package com.ddkk.concurrency;
/**
* DDKK.COM 弟弟快看,程序员编程资料站 16-4-4.
*/
public class Producer implements Runnable {
private Content content;
public Producer(Content content) {
this.content = content;
}
public void run() {
boolean flag = true;
for (int i = 0; i < 6; i++){
if (flag){
try {
content.set("authorA","titleA");
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
}else {
try {
content.set("authorB","titleB");
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
}
}
}
}
接着,我们定义消费者,消费者的消费任务也放在线程中单独执行:
package com.ddkk.concurrency;
/**
* DDKK.COM 弟弟快看,程序员编程资料站 16-4-4.
*/
public class Consumer implements Runnable {
private Content content;
public Consumer(Content content) {
this.content = content;
}
public void run() {
for (int i = 0; i < 6; i++){
try {
content.get();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
最后,就是我们的测试代码了:
package com.ddkk.concurrency;
/**
* DDKK.COM 弟弟快看,程序员编程资料站 16-4-4.
*/
public class ProducerConsumerModeTest {
public static void main(String[] args){
Content content = new Content();
Thread producer = new Thread(new Producer(content),"Producer");
Thread consumer = new Thread(new Consumer(content),"Consumer");
producer.start();
consumer.start();
}
}
运行上面的测试代码,得到的结果如下:
从运行结果可以看出,只有当生产者生产了数据后消费者才会从中取数据,这样取到的数据就不会重复。由一点要注意的是:消费者线程循环的次数必须和生产者线程一致,不然就会出现消费者一直等待生产者生产数据的情况,这点从代码中就可以看出,因为如果生产者没有继续生产的话消费者调用wait方法便会阻塞等待直到有数据。