一.生产者消费者模型的介绍
生产者消费者模型是一种常见的并发设计模式,用于解决生产者和消费者之间的数据共享和通信问题。在生产者消费者模型中,生产者负责生产数据并将其放入一个缓冲区中,而消费者从缓冲区取出数据进行处理。生产者和消费者可以同时运行,通过数据缓冲区进行数据的协调。
二.基于阻塞队列的生产者消费者模型
生产者消费者模型就比如现实生活中的包饺子,包饺子时如果每个人负责擀饺子,包饺子,各司其职效率会很低。假设一个人负责擀饺子皮,另一个负责包饺子效率就会很高。擀饺子皮的人为生产者,包饺子的人为消费者,盖帘相当于交易场所。生产者消费者模型为把要做的工作模块化,各司其职提供效率,彼此又产生联系,降低模块之间的耦合。
一.解耦合
如果有2个服务器A和B,A负责给B传输数据,B负责接收A的数据,当A挂了必然影响B,B挂了也影响A,A和B的耦合比较明显。假设在添加一个C,A同时给B和C传数据,当B挂了C还正常运行,也影响不大,但是增加C对代码的改动比较大,所以也不建议使用这种方式。
如果引用生产者消费者模型,增加一个阻塞队列就能有效解决这个问题。
当服务器A给B发送数据时,为了解耦合,可以引入阻塞队列。A发送的数据由阻塞队列来接收,B要获取A发来的数据,只需要从阻塞队列中获取数据即可。A挂了对B影响不是很大,B挂了对A影响也不是很大。如果要增加服务器C,只需要C和阻塞队列产生联系,代码改动较小,也很好的解耦合。
二.削峰填谷
如果没有引入阻塞队列,让A和B直接关联,当有一天A的访问量突然暴增,B承受不了A发送过来的数据,此时B就有可能挂了。引入阻塞队列后由于A和B并没有直接关联,A发送的数据突然暴增直接影响不了B。A发送的数据由阻塞队列接收,尽管数据比以往多很多,但B还是按照以往的速度从阻塞队列里面拿数据,B不至于挂了。相当于队列起到了一个缓冲的作用,这就是所谓的削峰。峰值只是暂时的,当峰值消退时,A发送的请求越来越少,B还是按照原来的速度从阻塞队列里面获取数据,B也不至于太空闲,这就是所谓的填谷。
三.实现生产者消费者模型
一.java标准库的阻塞队列
java标准库中已经实现了阻塞队列。
1.基于顺序表实现的阻塞队列:
2.基于链表实现的阻塞队列:
3.基于优先级队列实现的阻塞队列:
点开源码我们发现有add,offer,remove等方法,但是不建议使用这些方法。这些方法在数据结构的时候我们已经学过,只是普通的方法,不带有阻塞功能,用这些方法添加和删除元素只是往普通队列增加删除元素。要想带有阻塞功能我们可以使用put(添加元素),take(删除元素)。
import java.util.concurrent.*;
public class Test2 {
public static void main(String[] args) throws InterruptedException{
BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque();
blockingQueue.put(1);
blockingQueue.put(2);
blockingQueue.put(3);
System.out.println(blockingQueue);
blockingQueue.take();
System.out.println(blockingQueue);
}
}
二.阻塞队列实现生产者消费者模型
接着我们用库里面自带的阻塞队列实现一边生产元素一边消费元素的生产者消费者模型
public class Test {
public volatile static int count=0;
public static void main(String[] args) throws InterruptedException{
BlockingQueue<Integer> blockingQueue = new LinkedBlockingDeque<>();
Thread thread = new Thread(()->{
while (true){
System.out.println("生产的元素是" + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
blockingQueue.put(count);
count++;
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread thread1 = new Thread(()->{
while (true) {
try {
int n = blockingQueue.take();
System.out.println("消费的元素是" + n);
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread1.start();
thread.start();
}
}
三.手动实现基于循环队列的生产者消费者模型
一.循环队列的回顾
学习数据结构的时候我们已经学过循环队列,那么还记得循环队列是怎么判满,判空,放元素,取元素的吗?
一般开辟循环队列的空间大小时,要往往空出一个位置。比如我们希望数组的长度是3,那么开辟的实际空间是4,这样能更好的区分循环队列满和空的条件。如果不多开辟空间,循环队列满和空的条件都是rearfront,多开辟一块空间,循环队列满的条件为(tail+1)%length = =front,也就是tail走到多开辟的这块空间时,队列已经放满。最后一块空间不是为了放元素,是为了判满。
如果rear%lengthfront说明队列为空。
放入元素的时候先判满,满了不能放,然后tail位置上放入元素,让tail走到下一个位置。比如第一次0号下标放入“hello”
注意一定不能写tail++,队列元素已满的时候,再次++会越界,加1取模会让tail在次回到头节点,从而循环起来。
同理删除元素也让头节点++。注意也不能直接++,必须取模,其条件为 front = (front + 1) % length;
有了前置知识,那么我们首先实现一个循环队列。
二.实现循环队列
class BlockingQueue1{
public int[] elum;
public int front=0;
public int tail = 0;
public BlockingQueue1(){
this.elum= new int[1000];
}
public void put(int value){
if((tail+1)%elum.length==front){
return;
}
elum[tail]=value;
System.out.println("生产的元素是"+value);
tail=(tail+1)%elum.length;
}
public void take(){
if(front%elum.length==tail){
return;
}
int n = elum[front];
System.out.println("消费的元素是"+n);
front=(front+1)%elum.length;
}
}
public class Test3 {
public static void main(String[] args) {
BlockingQueue1 blockingQueue1 = new BlockingQueue1();
blockingQueue1.put(1);
blockingQueue1.put(2);
blockingQueue1.take();
}
}
该代码只是一个普通的循环队列,并没有阻塞功能,我们想让其没有满的时候阻塞,直到删除元素从阻塞中恢复。让空的时候也阻塞,直到添加新的元素后从阻塞中恢复。要实现该功能,我们很自然的想到wait。sleep和wait一样都具有阻塞功能,但sleep通常是等待一定的时间恢复,wait需要唤醒,如果没有唤醒就一直等下去,这就是我们想要的效果,当条件为空为满的时候一直阻塞直到条件不满足时从阻塞中恢复。那么对其修改如下。
三.带有阻塞功能的循环队列
class BlockingQueue1{
public int[] elum;
public int front=0;
public int tail = 0;
public BlockingQueue1(){
this.elum= new int[1000];
}
public void put(int value){
synchronized (this) {
if ((tail + 1) % elum.length == front) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
elum[tail] = value;
System.out.println("生产的元素是" + value);
tail = (tail + 1) % elum.length;
this.notify();
}
}
public void take() {
synchronized (this) {
if (front % elum.length == tail) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
int n = elum[front];
System.out.println("消费的元素是" + n);
front = (front + 1) % elum.length;
this.notify();
}
}
}
public class Test3 {
public static void main(String[] args) {
BlockingQueue1 blockingQueue1 = new BlockingQueue1();
blockingQueue1.put(1);
blockingQueue1.put(2);
blockingQueue1.take();
}
}
猜想一下,当前Put操作因为队列满了,wait发生阻塞,过了一段时间,wait被唤醒,唤醒的时候,此时的队列一定就不满了吗?是否可能队列还是满着呢?假设出现这种情况,唤醒之后,队列还是满着的。此时意味着接下来的代码继续执行,就可能把之前存入的元素给覆盖掉了。
我们已经知道wait不一定只能有notify唤醒,可能也会让intrrupt唤醒。假设队列已经为满,
程序运行到wait时停止,当wait被intrrupt唤醒时,此时并没有删除元素,队列此时任然为满,程序继续执行wait后面的代码往队列里面添加元素,程序会出现错误。解决这个问题必须让程序一直检测队列是否为满,只需要把if条件改为while条件,让其一直检测,即使wait被intrrupt唤醒,但队列还是为满一直在循环条件里面,直到删除了某个元素循环条件不满足时。
class BlockingQueue1{
public int[] elum;
public int front=0;
public int tail = 0;
public BlockingQueue1(){
this.elum= new int[1000];
}
public void put(int value){
synchronized (this) {
while ((tail + 1) % elum.length == front) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
elum[tail] = value;
System.out.println("生产的元素是" + value);
tail = (tail + 1) % elum.length;
this.notify();
}
}
public void take() {
synchronized (this) {
while (front % elum.length == tail) {
try {
this.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
int n = elum[front];
System.out.println("消费的元素是" + n);
front = (front + 1) % elum.length;
this.notify();
}
}
}
public class Test3 {
public static void main(String[] args) {
BlockingQueue1 blockingQueue1 = new BlockingQueue1();
blockingQueue1.put(1);
blockingQueue1.put(2);
blockingQueue1.take();
}
}
四.内存可见性
该代码在多线程下还要考虑内存可见性问题,有可能某个线程对某个变量进行了修改,其他线程没有读到修改变量的值。给代码中就行数据修改的变量加上voliatle,同时在多线程下启动就能解决内存可见性问题。
class BlockingQueue1{
public int[] elum;
public volatile int count=0;
public volatile int front=0;
public volatile int tail = 0;
public Object object = new Object();
public BlockingQueue1(){
this.elum= new int[1000];
}
public void put(int value){
synchronized (object) {
while ((tail + 1) % elum.length == front) {
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
elum[tail] = value;
tail = (tail + 1) % elum.length;
object.notify();
}
}
public int take() {
synchronized (object) {
while (front % elum.length == tail) {
try {
object.wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
int n = elum[front];
front = (front + 1) % elum.length;
object.notify();
return n;
}
}
}
public class Test3 {
public static void main(String[] args) {
BlockingQueue1 blockingQueue1 = new BlockingQueue1();
Thread thread = new Thread(()->{
while (true){
System.out.println("生产的元素是" + blockingQueue1.count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
blockingQueue1.put(blockingQueue1.count);
synchronized (blockingQueue1.object) {
blockingQueue1.count++;
}
}
});
Thread thread1 = new Thread(()->{
while (true){
int ret = blockingQueue1.take();
System.out.println("消费的元素是"+ret);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread.start();
thread1.start();
}
}