一、Producer-Consumer设计模式
Producer是生产者的意思,指的是生产数据的线程,Consumer则是消费者的意思,指的是使用数据的线程。
生产者安全的将数据交给消费者,虽然仅是这样看似简单的操作,但生产者和消费者以不同的线程运行时,二者之间的处理速度就会引起差异。例如:消费者想消费数据,可生产者还没有生产出数据,或者生产者生产出了数据,可消费者还无法接受数据。
Producer-Consumer模式在生产者和消费者之间加入一个桥梁角色,改桥梁用于消除线程间处理速度的差异。一般来说,在该模式中,生产者和消费者都有多个。
二、示例程序
为了理解Producer-Consumer设计模式,我们先来看一个示例程序,在这个示例程序中,有3位蛋糕制作者制作蛋糕,并将蛋糕放到桌上,然后有3位客人来吃这些蛋糕;
- 蛋糕制作者(MakerThread)制作蛋糕,并放到桌子上;
- 桌上最多可以放置3个蛋糕;
- 如果桌上已经放满了3个蛋糕,必须等到桌上空出位置才能继续放;
- 客人(EaterThread)取桌上的蛋糕吃;
- 客人按照蛋糕放置到桌上的顺序来取蛋糕;
- 当桌上一个蛋糕都没有时,必须等桌上有蛋糕了才能继续取蛋糕吃。
类名 | 说明 |
---|---|
Main.java | 测试类 |
MakerThread.java | 蛋糕制作者的类 |
EaterThread.java | 蛋糕消费者 |
Table.java | 表示桌子的类 |
1.Main类
会创建一个桌子的实例,并启动表示糕点师和客人的线程。
package com.viagra.Producer_Consumer_Pattern.Lesson1;
/**
* @Auther: viagra
* @Date: 2019/11/19 17:18
* @Description:
*/
public class Main {
public static void main(String[] args) {
Table table = new Table(3);
new MakerThread("MakerThread-1", table, 31415).start();
new MakerThread("MakerThread-2", table, 92653).start();
new MakerThread("MakerThread-3", table, 58979).start();
new EaterThread("EaterThread-1", table, 32384).start();
new EaterThread("EaterThread-2", table, 62643).start();
new EaterThread("EaterThread-3", table, 38327).start();
}
}
2.MakerThread类
用户制作蛋糕,并将蛋糕放到桌上。MakerThread会先暂停一段随机时长,然后再调用Table类的put方法,将制作好的蛋糕放到桌上。
package com.viagra.Producer_Consumer_Pattern.Lesson1;
import java.util.Random;
/**
* @Auther: viagra
* @Date: 2019/11/19 17:18
* @Description:
*/
public class MakerThread extends Thread {
private final Random random;
private final Table table;
private static int id = 0; //蛋糕的流水号
public MakerThread(String name, Table table, int seed) {
super(name);
this.table = table;
this.random = new Random(seed);
}
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000));
String cake = "[ Cake No. " + nextId() + " by " + getName() + "]";
table.put(cake);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized int nextId() {
return id++;
}
}
3.EaterThread类
表示从桌子上取蛋糕的客人,通过Table类的take方法取桌上的蛋糕,也暂停一段随机时长。
package com.viagra.Producer_Consumer_Pattern.Lesson1;
import java.util.Random;
/**
* @Auther: viagra
* @Date: 2019/11/19 17:18
* @Description:
*/
public class EaterThread extends Thread {
private final Random random;
private final Table table;
public EaterThread(String name, Table table, long seed) {
super(name);
this.table = table;
this.random = new Random(seed);
}
public void run() {
try {
while (true) {
String cake = table.take();
Thread.sleep(random.nextInt(1000));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
4.Table类
用户表示放蛋糕的桌子,蛋糕以String类型表示,Table声明了一个String数据类型的字段,用于作为蛋糕的实际放置位置。
为了正确放置put和取take蛋糕,Table类还声明了int类型的字段tail,head和count:
- tail字段,表示下一次放置put蛋糕的位置;
- head字段,表示下一次取take蛋糕的位置;
- count字段,表示当前桌子上蛋糕的个数。
package com.viagra.Producer_Consumer_Pattern.Lesson1;
/**
* @Auther: viagra
* @Date: 2019/11/19 17:18
* @Description:
*/
public class Table {
private final String[] buffer;
private int tail;//下次put的位置
private int head;//下次take的位置
private int count;//buffer中的蛋糕数
public Table(int count) {
this.buffer = new String[count];
this.head = 0;
this.tail = 0;
this.count = 0;
}
/**
* put方法使用了Guarded Suspension模式,守护条件为while条件表达式的逻辑非运算!count >= buffer.length,也就是说当桌上放置的蛋糕小于最大
* 放置量,把它作为放置蛋糕的put方法守护条件.
* 通过上面的处理,蛋糕已经放置导桌上了,由于桌子的状态发生了变化,所以要执行notifyAll,唤醒所有正在wait的线程.
*
* @param cake
* @throws InterruptedException
*/
//放置蛋糕
public synchronized void put(String cake) throws InterruptedException {
System.out.println(Thread.currentThread().getName() + " puts " + cake);
while (count >= buffer.length) {
wait();
}
buffer[tail] = cake;
tail = (tail + 1) % buffer.length;
count++;
notifyAll();
}
/**
* take方法使用了Guarded Suspension模式,守护条件为!count <= 0.
*
* @return
* @throws InterruptedException
*/
//拿取蛋糕
public synchronized String take() throws InterruptedException {
while (count <= 0) {
wait();
}
String cake = buffer[head];
head = (head + 1) % buffer.length;
count--;
notifyAll();
System.out.println(Thread.currentThread().getName() + " takes " + cake);
return cake;
}
}
5.运行结果
MakerThread-2 puts [ Cake No. 0 by MakerThread-2]
EaterThread-2 takes [ Cake No. 0 by MakerThread-2]
MakerThread-1 puts [ Cake No. 1 by MakerThread-1]
EaterThread-1 takes [ Cake No. 1 by MakerThread-1]
MakerThread-3 puts [ Cake No. 2 by MakerThread-3]
EaterThread-3 takes [ Cake No. 2 by MakerThread-3]
MakerThread-2 puts [ Cake No. 3 by MakerThread-2]
EaterThread-1 takes [ Cake No. 3 by MakerThread-2]
MakerThread-3 puts [ Cake No. 4 by MakerThread-3]
EaterThread-3 takes [ Cake No. 4 by MakerThread-3]
MakerThread-1 puts [ Cake No. 5 by MakerThread-1]
EaterThread-2 takes [ Cake No. 5 by MakerThread-1]
MakerThread-2 puts [ Cake No. 6 by MakerThread-2]
EaterThread-1 takes [ Cake No. 6 by MakerThread-2]
MakerThread-3 puts [ Cake No. 7 by MakerThread-3]
EaterThread-3 takes [ Cake No. 7 by MakerThread-3]
MakerThread-1 puts [ Cake No. 8 by MakerThread-1]
EaterThread-1 takes [ Cake No. 8 by MakerThread-1]
MakerThread-2 puts [ Cake No. 9 by MakerThread-2]
EaterThread-3 takes [ Cake No. 9 by MakerThread-2]
MakerThread-1 puts [ Cake No. 10 by MakerThread-1]
EaterThread-3 takes [ Cake No. 10 by MakerThread-1]
MakerThread-1 puts [ Cake No. 11 by MakerThread-1]
EaterThread-1 takes [ Cake No. 11 by MakerThread-1]
MakerThread-3 puts [ Cake No. 12 by MakerThread-3]
EaterThread-2 takes [ Cake No. 12 by MakerThread-3]
MakerThread-3 puts [ Cake No. 13 by MakerThread-3]
EaterThread-3 takes [ Cake No. 13 by MakerThread-3]
MakerThread-2 puts [ Cake No. 14 by MakerThread-2]
EaterThread-3 takes [ Cake No. 14 by MakerThread-2]
MakerThread-2 puts [ Cake No. 15 by MakerThread-2]
EaterThread-2 takes [ Cake No. 15 by MakerThread-2]
MakerThread-3 puts [ Cake No. 16 by MakerThread-3]
EaterThread-3 takes [ Cake No. 16 by MakerThread-3]
MakerThread-1 puts [ Cake No. 17 by MakerThread-1]
EaterThread-1 takes [ Cake No. 17 by MakerThread-1]
MakerThread-1 puts [ Cake No. 18 by MakerThread-1]
EaterThread-2 takes [ Cake No. 18 by MakerThread-1]
MakerThread-3 puts [ Cake No. 19 by MakerThread-3]
EaterThread-2 takes [ Cake No. 19 by MakerThread-3]
6.示例程序的时序图
三、Producer-Consumer模式中登场角色
- Data:由Producer角色生成,共Consumer角色使用。
- Profucer角色生产Data角色,并将其传给Channel(通道)角色。
- Consumer角色从Channel(通道)角色获取Data角色并且使用。
- Channel角色从Producer角色获取Data,还会响应Consumer的请求,传递Data,为了确保安全性,Channel会对Profucer和Consumer访问执行互斥处理。
如图:
四、扩展思路
1.守护安全性的Channel角色(可复用性)
在Producer-Consumer模式中,承担安全守护责任的是Channel角色,Channel执行线程间的互斥处理,确保Producer正确地将Data传递给Consumer。
2.不可以直接传递吗?
Producer-Consumer模式为了从Producer正确地将Data传递给
(1)直接调用方法,如果Producer直接调用Consumer的方法,那么执行处理的就不是Consumer的线程,而是Producer的线程,这样执行处理的时间就必须由Producer的线程来承担,准备下一个数据的处理也会发生延迟,程序性能会变差。就好比:蛋糕师做好蛋糕,直接交给客人,在客人吃完后再做下一个蛋糕一样。
(2)插入Channel角色,Producer将Data传递给Channel后,无需等待就可以准备下一个Data了,Consumer吃蛋糕的线程完全不会影响Producer线程。
3.Channel的剩余空间所导致的问题
…
4.以什么顺序传递给Data?
(1)队列-先接收的先传递
最通常的方法就是先接收先传递,这种方法成为FIFO,先进先出或者队列。
(2)栈-后接收的先传递
与队列相反,该方法是后接收的先传递,我们将这种方法成为LIFO(last in frist out),后进先出或者栈。如:叠放碟子的场景等。
(3)优先队列-“优先”的先传递
比较常用的方法还有优先队列,channel给接收到的Data赋予优先级,优先级高的先被传递出去,而优先级的确定方法千差万别。
5.“存在中间角色”的意义
Channel最为中间角色具有非常重要的意义,可以实现线程的协调运行。
- 线程的协调运行要考虑“放在中间的东西”;
- 线程的互斥处理要考虑“应该保护的东西”。
协调运行和互斥处理其实是内外统一的,为了让线程协调运行,必须执行互斥处理,以防止共享的内容被破坏。而线程的互斥处理是为了线程的协调运行才执行的。因此,协调运行和互斥处理之间有着很深的关系。