1.1 解决什么问题
生产者消费者模式的一个典型应用场景就是“分布式系统”。那什么是分布式系统呢?
在程序有很多很多用户访问的时候,一个服务器的资源就可能不够用,这个时候可以用多个服务器来拆分功能配合使用,每个服务器来负责一部分功能,通过通信来和其他服务器进行配合工作,单个服务器实现的功能少了,那么消耗的资源也就少了。这种多个服务器互相配合完成所有功能,就是“分布式系统”。
如图就是一个简易版本的分布式系统,由客户端发送请求给服务器a,服务器a依靠服务器b,c响应的数据来完成整个功能,服务器a就叫消费者,服务器b、c就叫生产者。
简而言之,提供数据的就是生产者,使用生产者提供数据来完成功能的就是消费者。
在这种情况下,生产者和消费者都是直接通信的,服务器a高度依赖服务器b,c来完成全部功能。如果服务器b挂了,那么服务器a立刻就不能打视频电话了,如果服务器c要升级功能了,那么服务器a也立刻就不能提供看帖子的服务了。这时,服务器a和服务器b,c高度耦合。
高度耦合,就是a高度依赖b,如果b出了什么变化会影响到a。举个例子吧,只用电锅做饭的人高度依赖电网供电。如果有一天电网维修断电了,这个电锅通不了电,就做不了饭了。在计算机中高度耦合并不是一个良好的特性。我们要追求的是高内聚,低耦合。高内聚和低耦合其实都是一个意思,就是不要去过度的依赖一个东西。
生产者-消费者模型就是使用容器来降低生产者和消费者耦合程度。
我们可以在生产者和消费者之间加一个阻塞队列(也可以是用阻塞队列实现的程序,或者是部署了阻塞队列的服务器),让生产者和消费者通过队列来进行通信,消费者把请求发送到队列里,消费者从队列中拿生产者发送的数据,生产者从队列中获取请求,把返回的响应发送到队列里。生产者和消费者不直接通信。这样就可以使生产者和消费者解耦。
1.2 阻塞队列
阻塞队列是一种数据结构,在队列(先进先出)的基础上增加了阻塞特性,阻塞特性就是:
-
在队列满的时候,入队列操作会进入阻塞,当队列不满的时候再解除阻塞,继续入队列。
-
在队列空的时候,出队列操作对进入阻塞,等队列不空的时候再解除阻塞,继续出队列。
java中实现阻塞队列的类有很多,它们都实现了java.util.concurrent包下的BlockingQueue接口。下面来模拟实现一下基于数组实现的阻塞队列(在java中的类是ArrayBlockingQueue):
注意,notify方法是无差别唤醒,只要同一个锁对象里的操作进入阻塞,notify就能唤醒,所以有的时候线程被唤醒,不是因为等待的条件满足了,而是因为被“误伤"了,所以需要再次进行条件判断。所以wait往往搭配while来进行使用。
出队列和入队列的锁对象必须相同,因为这样才能构成锁竞争,保证线程安全。
1.3 实现方式
生产者就是生产数据的那一方,消费者就是需要使用生产者生产的数据的一方。生产者消费者在实际中有可能是线程,有可能是一个独立的服务器程序,还有可能是一组独立的服务器程序。
实现生产者消费者模型,就是将生产者生产的数据放入阻塞队列里,当阻塞队列满了,生产者就不能向阻塞队列放数据,当有元素从阻塞队列删除的时候,生产者就可以继续放数据了。消费者从阻塞队列中取生产者生产的数据使用,当阻塞队列为空的时候,消费者不能从阻塞队列取数据,当阻塞中添加数据的时候,消费者被就可以继续取数据了。
public static void main(String[] args) throws InterruptedException {
MyBlookingQueue queue = new MyBlookingQueue(1000);
// 生产者
Thread t1 = new Thread(() -> {
int n = 1;
while (true) {
try {
queue.put(n + "");
System.out.println("生产元素 " + n);
n++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 消费者
Thread t2 = new Thread(() -> {
while (true) {
try {
String n = queue.take();
System.out.println("消费元素 " + n);
Thread.sleep(500);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t1.start();
t2.start();
}
这里使用的阻塞队列是自己简单实现的,java中有阻塞队列功能的有很多类,这些类都实现了java.util.concurrent地下的BlookingQueue接口,比如有以下这些类:
ArrayBlockingQueue:基于数组实现的有界阻塞队列。
LinkedBlockingQueue:基于链表实现的可选有界或无界阻塞队列。
PriorityBlockingQueue:支持优先级的无界阻塞队列。
DelayQueue:用于存放实现了 Delayed 接口的元素,按照延迟时间顺序被消费。
SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,反之亦然。
LinkedTransferQueue:基于链表实现的无界阻塞队列,支持更丰富的操作,如 transfer 和 tryTransfer。
接下来用java中的LinkedBlockingQueue来实现一下:
package thread;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
public class ThreadDemo4 {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new LinkedBlockingDeque<>(10);
Thread t1 = new Thread(() -> {
int n = 1;
while (true) {
try {
queue.put(n);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("生产元素:" + n);
n++;
}
});
Thread t2 = new Thread(() -> {
Integer take = null;
while (true) {
try {
Thread.sleep(2000);
take = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("消费元素:" + take);
}
});
t1.start();
t2.start();
}
}
1.4 实际意义
1.4.1 使生产者和消费者解耦合
生产者不需要知道消费者是谁,不关心消费者如何使用数据,消费者不需要知道生产者是谁,不关心生产者如何生产数据,生产者和消费者通过容器来进行通信。就算生产者更改生产数据的方式,也不会影响到消费者使用数据,就算消费者改变使用数据的方式,也不会影响到,生产者生产数据。
1.4.2 平衡生产者和消费者的数据处理能力(削峰填谷)
有的时候消费者取数据的速度远远大于生产者生产数据的速度,那么队列空的时候,消费者就进入阻塞状态,取不了数据,等生产者生产数据的时候才能从阻塞状态解除,继续去取数据。
有的时候生产者生产数据的速度远远大于消费者取数据的速度,那么队列满的时候,生产者就进入阻塞状态,生产不了数据,等消费者取数据的时候才能从阻塞状态解除,继续去生产数据。