实际的软件开发过程中,经常会碰到如下场景:某个模块负责产生数据,这些数据由另一个模块来负责处理(此处的模块是广义的,可以是类、函数、线程、进程等)。产生数据的模块,就形象地称为生产者;而处理数据的模块,就称为消费者。单单抽象出生产者和消费者,还够不上是生产者/消费者模式。该模式还需要有一个缓冲区处于生产者和消费者之间,作为一个中介。生产者把数据放入缓冲区,而消费者从缓冲区取出数据。大概的结构如下图。
-
生产者
数据的提供方可形象地称为数据的生产者,它“生产”了数据, -
消费者
而数据的加工方则相应地被称为消费者,它“消费”了数据。实际上,生产者“生产”数据的速率和消费者“消费”数据的速率往往是不均衡的,比如数据的“生产”要比其“消费”快。为了避免数据的生产者和消费者中处理速率快的一方需要等待处理速率慢的一方, -
缓冲区
Producer-Consumer模式通过在数据的生产者和消费者之间引入一个通道(Channel,暂时可以将其简单地理解为一个队列)对二者进行解耦(Decoupling):生产者将其“生产”的数据放入通道,消费者从相应通道中取出数据进行“消费”(处理),生产者和消费者各自运行在各自的线程中,从而使双方处理速率互不影响。
1.示例
1.1 定义生产者
class Producer extends Thread {
private Buffer buffer;
private int number;
public Producer(Buffer b, int number) {
buffer = b;
this.number = number;
}
public void run() {
for (int i = 0; i < 10; i++) {
try {
// 模拟生产数据
sleep(500);
} catch (InterruptedException e) {
}
// 将数据放入缓冲区
buffer.put(i);
System.out.println("生产者 #" + this.number + " put: " + i);
}
}
}
1.2 定义消费者
class Consumer extends Thread {
private Buffer buffer;
private int number;
public Consumer(Buffer b, int number) {
buffer = b;
this.number = number;
}
public void run() {
int value;
for (int i = 0; i < 10; i++) {
// 从缓冲区中获取数据
value = buffer.get();
try {
// 模拟消费数据
sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("消费者 #" + this.number + " got: " + value);
}
}
}
1.3 定义缓冲区
class Buffer {
private List<Integer> data = new ArrayList<>();
private static final int MAX = 10;
private static final int MIN = 0;
public synchronized int get() {
while (MIN == data.size()) {
try {
wait();
} catch (InterruptedException e) {
}
}
Integer i = data.remove(0);
notifyAll();
return i;
}
public synchronized void put(int value) {
while (MAX == data.size()) {
try {
wait();
} catch (InterruptedException e) {
}
}
data.add(value);
notifyAll();
}
}
2.模式优点
Producer-Consumer模式使得“产品”的生产者和消费者各自的处理能力(速率)相对来说互不影响。生产者只需要将其“生产”的“产品”放入通道中就可以继续处理,而不必等待相应的“产品”被消费者处理完毕。而消费者运行在其自身的工作者线程中,它只管从通道中取“产品”进行处理,而不必关心这些“产品”由谁“生产”以及如何“生产”这些细节。因而消费者的处理能力相对来说又不影响生产者,同时又与生产者是松耦合(Loose Coupling)的关系。另一方面,当消费者处理能力比生产者处理能力大的时候,可能出现通道为空的情形,此时消费者的工作者线程会被暂挂直到生产者“生产”了新的“产品”。此时出现了事实上的消费者等待生产者的情形。类似地,当消费者的处理能力小于生产者的处理能力时,通道可能会满,导致生产者线程被暂挂直到消费者“消费”了通道中的部分“产品”而腾出了存储空间。此时出现了事实上的生产者等待消费者的情形。生产者和消费者各自的处理能力相互不影响是相对的。
-
并发 (异步)
生产者直接调用消费者,两者是同步(阻塞)的,如果消费者吞吐数据很慢,这时候生产者白白浪费大好时光。而使用这种模式之后,生产者将数据丢到缓冲区,继续生产,完全不依赖消费者,程序执行效率会大大提高。 -
解耦
生产者和消费者之间不直接依赖,通过缓冲区通讯,将两个类之间的耦合度降到最低。 -
削峰填谷
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。等生产者的制造速度慢下来,消费者再慢慢处理掉。
总结
Producer-Consumer模式可以看作模式的模式,即许多模式可以看作该模式的一个实例,
java.util.concurrent.ThreadPoolExecutor可以看成是Producer-Consumer模式的可复用实现。ThreadPoolExecutor内部维护的工作队列和工作者线程相当于Producer-Consumer模式的Channel参与者和Consumer参与者。而ThreadPoolExecutor的客户端代码则相当于Producer参与者。利用ThreadPoolExecutor实现Producer-Consumer模式,
多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。
https://github.com/forestnlp/concurrentlab
如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。
您的支持是对我最大的鼓励。