聊聊生产者和消费者模式的工作原理

一 前言

生产者和消费者模式大家应该都比较熟悉,所以今天的文章就比较简单,主要跟大家简单介绍一下生产者和消费者的工作原理,然后再介绍一下使用这个模式会带来哪些好处。

二、生产者和消费者模式的工作原理

其实生产者和消费者模式中有三个重要角色,生产者、任务队列、消费者。生产者提交任务到队列,消费者从队列中取出任务进行处理。

大家可以结合下面的示意图来理解一下。如图1:

这个图对应到我们程序中的话,那么生产者就是指一个线程,消费者是另外一个线程,而中间的任务队列呢,它是一个数据结构,用来存放任务数据。

大家再看一下这个图。如图2所示:

上图中生产者跟消费者都只画了一个线程,但其实它们都是可以是多线程的。如图3所示:

而且生产者线程数量跟消费者线程的数量是不用对等的,比如上图生产者线程有三个,而消费者线程只有两个。

生产者和消费者线程的话,在实际的开发中一般选择JDK自带的线程池来实现,至于中间的任务队列,由于多线程并发操作,需要线程安全问题,还有就是队列一般也会做限制长度,队列满了之后生产者线程需要阻塞,队列空了之后消费者线程需要阻塞,可以直接选择JDK自带的线程安全的有界的阻塞队列。

下面贴一段生产者和消费者模式的示例代码,大家可以对着参考一下。

public class Test {
public static void main(String[] args) {
// 生产者线程池
ExecutorService producerThreads = Executors.newFixedThreadPool(3);
// 消费者线程池
ExecutorService consumerThreads = Executors.newFixedThreadPool(2);
// 任务队列,长度为10
ArrayBlockingQueue<Task> taskQueue = new ArrayBlockingQueue<Task>(10);
// 生产者提交任务
producerThreads.submit(() -> {
try {
taskQueue.put(new Task("任务"));
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 消费者处理任务
consumerThreads.submit(() -> {
try {
Task task = taskQueue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
static class Task {
// 任务名称
private String taskName;
public Task(String taskName) {
this.taskName = taskName;
}
}
}

三、生产者和消费者模式的好处

生产者和消费者模式是并发编程中一个非常经典的设计模式,有很多的使用场景,比如现在流行的微服务系统开发中,经常会用到的MQ中间件也是生产者和消费者模式。

下面我们来罗列一下看看采用生产者和消费者模式有哪些优点?

由于中间任务队列的存在,生产者线程只负责生产任务,并将任务添加到任务队列中,不用关心任务的后续处理。而消费者这边呢,消费者线程只负责从任务队列中获取任务并执行即可。

生产者跟消费者之间没有任何依赖,从架构设计的角度来看,这就是解耦。如图4所示:

与异步相对的是同步,传统的方法之间的调用是同步的。同步调用与异步调用最主要的区别就在于客户端完成一次请求的响应时间不一样。

我们画个图,先来看一下传统的方法之间同步调用。如图5所示:

图中的线程就表示客户端,由于方法之间的调用是同步的,假设方法A执行完本身的业务逻辑需要100毫秒,方法B执行完本身的业务逻辑需要500毫秒,那么线程完成这次请求总的响应时间就是600毫秒,而且如果方法B还同步调用其它方法的话,那么总的响应时间还会更长。

在实际的业务场景中,如果方法A的业务逻辑执行不依赖方法B的返回结果的话,那么方法A对方法B的调用可以改成异步调用。

我们再画个图,看一下异步调用的情况。如图6所示:

由于方法A对方法B的调用是异步的,线程调用方法A执行完业务逻辑之后就直接返回了,不用等方法B执行完,那么此时线程完成这次请求总的响应时间就只要100毫秒。

在生产者和消费者模式中,生产者线程只需要将任务添加到任务队列,而无需等待任务被消费者线程执行完,这也就是说任务的生产和消费是异步的。

前面说了异步处理,大家或许会有这样的疑问,异步处理最简单的方式就是创建一个新的线程去处理,那么生产者和消费者模式中增加的“任务队列”是做什么用的呢?

因为实际上单个生产者“生产”任务和单个消费者“消费”任务的速率往往是不均衡的,我们假设生产者“生产”任务的速率比较慢,而消费者“消费”任务的速率比较快,比如是1:5。

如果生产者有5个线程,那么采用创建新的线程的方式发起异步调用的话,就需要再创建5个新的线程,但是在生产者和消费者模式中,由于中间“任务队列”的存在,消费者线程就只需要1个就可以了,如图7所示:

图片

图7

我们都知道在计算机当中,创建的线程越多,CPU进行上下文切换的成本就越大,所以我们在编程的时候创建的线程并不是越多越好,而是适量即可,采用生产者和消费者模式就可以很好的支持我们使用适量的线程来完成任务。

如果在某一段业务高峰期的时间里生产者“生产”任务的速率很快,而消费者“消费”任务速率很慢,由于中间的任务队列的存在,也可以起到缓冲的作用,我们在使用MQ中间件的时候,经常说的削峰填谷也就是这个意思。如图8所示:

声明:文中观点不代表本站立场。本文传送门:https://eyangzhen.com/242931.html

联系我们
联系我们
分享本页
返回顶部