目录
生产者消费者模型
为了更好的理解阻塞队列的使用常见,首先来理解这个生产者消费者模型。
假设现在是过年期间,大家其乐融融,围在一起包饺子
假设四个滑小稽用这种模式来包饺子:自己擀一个面皮-包一个饺子。
这样虽然可以完成包饺子的任务,但并不高效,并且只有一个擀面杖,四个滑小稽还需要对这个擀面杖进行竞争。为了提高效率,他们换了一种包饺子策略
一个滑小稽负责擀面皮,三个滑小稽负责包
这样就构成了一个最简单的生产者-消费者模型
生产者负责生产资源,消费者负责消耗资源,(这里的资源指的就是饺子皮)而两者需要一个交易场所进行交互资源,这里的交易场所就是桌子
而这里桌子又具有这样的特性
- 如果负责擀面皮的滑小稽的速度>三个负责包饺子的滑小稽的速度:桌子上一直有面皮,负责包饺子的滑小稽可以一直包饺子
- 如果负责擀面皮的滑小稽的速度<三个负责包饺子的滑小稽的速度:桌子每放上一个面皮就会迅速被三个负责包饺子的滑小稽消耗掉,一旦桌子上没有面皮,三个负责包饺子的滑小稽就会陷入阻塞等待的状态。
而在JAVA中,这里的桌子就被称为:阻塞队列(BlockingDeque)
生产者消费者模型的初心
生产者消费者模型的初心主要是为了解决两个方面
- 让上下游模块间进行更好的“解耦合”
- 削峰填谷
耦合
指的是两个模块之间的关联性关系,关联越强,耦合性越高,反之越低
例如你的亲人生病了,你就要放下手头的工作去陪他,这对你原来对自己的工作生活安排是有较大影响的,这就是高耦合
如果是你并不熟的同学/同事生病了,你就只需要在微信上表达一下关心,不需要调整原来的生活工作安排,这就是低耦合
内聚
内聚是指模块内各元素(比如函数、类等)之间彼此联系紧密,共同完成某一个功能或者单一目的的度量
例如你和你的女朋友同居了,你的生活习惯很不好,对于穿过的衣服,随手乱丢,到处摆放,而你的女朋友对于穿过的衣服则是耐心整理,归类存放,这样在日后你需要找一件衣服时,你就会遍历整个屋子去寻找,这就是低内聚。而你的女朋友找一件衣服时,只需要遍历她分类的那个衣柜即可。这就是高内聚
解耦合
我们来考虑这样一个场景:有A B两个服务器
此时A和B直接交换数据,就可以说A、B是高耦合的,因为如果A挂了就会直接影响B,而B挂了也会直接影响A,而且如果此时我们想要加入一个新的服务器C,还会涉及到比较麻烦的调整。
为了解决这个问题,我们就可以使用阻塞队列来解耦合
此时就用到了生产者-消费者模型,A是生产者,B是消费者,此时A和B都不知道彼此的存在,只通过一个阻塞队列来进行数据交换。
而此时在加入服务器C只需要让C从队列中取元素即可。
削峰填谷
同样是A和B两个服务器直接关联,A负责接收请求,B负责处理请求。用户行为会影响A,而用户行为是随机的情况,有些情况下A就会出现一波“峰值”,爆发性增长一波。
服务器的处理资源能力都是有限的,因为服务器处理每个请求都需要消耗硬件资源,很有可能A受到过多的请求导致硬件资源达到瓶颈,进而导致B挂掉了。
为了解决这个问题,就可以使用这样的模型
还是使用阻塞队列来关联A-B,A收到的请求多了,队列中的元素也就多了,但是B仍然可以按照固定的速率来处理请求,这样队列就帮B承担了压力,这就叫“削峰”。
峰值过后,A不在接收大量数据,此时数据积压在队列中,B仍然可以在非高峰期以稳定的速率来处理请求,这就叫做“填谷”。
当然队列也有挂的风险,但队列是一个相对稳定的代码,不像A、B,要频繁更改需求,更改代码,队列挂掉的风险远远小于A、B。
阻塞队列的使用
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
public class work2 {
public static void main(String[] args) throws InterruptedException{
BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();//双端阻塞队列
//消费者 从队列中取元素
Thread t1 = new Thread(()->{
while (true){
try {
int val = queue.take();
System.out.println("消费元素:"+val);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//生产者,放元素到队列中
Thread t2 = new Thread(()->{
int i = 0;
while (true){
try {
int val = i;
i++;
queue.put(val);
System.out.println("生产元素:"+val);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
这就是一个生产者-消费者模型的阻塞队列代码案例。
import java.util.concurrent;
BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();//双端阻塞队列
这是java中自带的阻塞队列,BlockingDeque,它是一个接口,LinkedBlockingDeque是一个基于链表实现的双端阻塞队列,它实现了BlockingDeque这个接口,在java.util.concurrent这个包中。
- take():是阻塞队列中取元素的方法
- put():是阻塞队列中入元素的方法
这里的t1、t2分别代表了消费者-生产者,其中t2一直在生产元素,速率为每秒生产一个,t1一直在消费元素,速率不做限制,运行结果为
可以看到,t2每生产一个元素t1会立马消耗掉,如果队列中没有元素,t1会进行阻塞等待。
代码模拟实现阻塞队列
实现一个阻塞队列,我们可以分为三步
- 1.实现一个普通队列
- 2.加上线程安全
- 3.加上阻塞功能
实现一个普通队列
我们这里使用数组来实现一个循环队列,一个循环队列应该有这样一些属性
- 一个数组用于存放元素
- head变量用于记录队列头
- tail变量用于记录队列尾
并且有这样一些方法
- put()入队列
- take()出队列
还有这样一些特性
- 先进先出,后进后出
- 当tail走到数组尾时,如果队列不为满,将tail放到数组首地址,循环利用空间
由于队列为空和队列为满时,tail和head都是重合的,那么如何判断这两个条件呢
- 浪费一个空间,即当(tail +1) % arr.length == head)时判定为满,head == tail时判定为空
- 获取队列元素个数,即添加一个变量来记录队列元素个数,当队列元素等于数组长度时说明队列满
下面来写代码
class MyBlockingQueue{
public int[] nums = new int[100];
public int head = 0;
public int tail = 0;
public int size = 0;
//插入
public void put(int val){
//判定是不是满
if(size == nums.length){
//满了
System.out.println("队列满");
return;
}
nums[tail] = val;
tail++;
//如果tail到尾,让tail从头
if(tail == nums.length){
tail = 0;
}
size++;
}
//出队列
public Integer take(){
//判定是否为空
if(tail == head){
System.out.println("队列空");
return null;
}
int val = nums[head];
head++;
if(head == nums.length){
head = 0;
}
size--;
return val;
}
}
加上线程安全
在上面的代码中,涉及到各种读、写操作,在前面的文章中介绍过,多线程中由于线程随机调度,多个线程同时修改一个变量时会出现问题,多个线程读一个变量时也会出现问题,解决办法就是加锁和volatile关键字
一个很简单粗暴的办法,把所有涉及到写操作的方法加上synchronized,所有涉及到读操作的变量加上volatile。
class MyBlockingQueue{
public int[] nums = new int[100];
volatile public int head = 0;
volatile public int tail = 0;
volatile public int size = 0;
//插入
synchronized public void put(int val){
//判定是不是满
if(size == nums.length){
//满了
System.out.println("队列满");
return;
}
nums[tail] = val;
tail++;
//如果tail到尾,让tail从头
if(tail == nums.length){
tail = 0;
}
size++;
}
//出队列
synchronized public Integer take(){
//判定是否为空
if(tail == head){
System.out.println("队列空");
return null;
}
int val = nums[head];
head++;
if(head == nums.length){
head = 0;
}
size--;
return val;
}
}
加上阻塞功能
阻塞功能是这个队列的灵魂所在,现在我们给它加上
需要加上的功能有
- 队列满,队列阻塞,当有元素出队列时,唤醒队列
- 队列空,队列阻塞,当有元素插入队列时,唤醒队列
修改后的代码如下
class MyBlockingQueue{
public int[] nums = new int[100];
volatile public int head = 0;
volatile public int tail = 0;
volatile public int size = 0;
//插入
synchronized public void put(int val)throws InterruptedException{
//判定是不是满
while(size == nums.length){
//满了
System.out.println("队列满");
this.wait();//因为这里的锁对象就是this
}
nums[tail] = val;
tail++;
//如果tail到尾,让tail从头
if(tail == nums.length){
tail = 0;
}
size++;
this.notify();
}
//出队列
synchronized public Integer take() throws InterruptedException{
//判定是否为空
while(size == 0){
System.out.println("队列空");
this.wait();//因为这里的锁对象就是this
}
int val = nums[head];
head++;
if(head == nums.length){
head = 0;
}
size--;
this.notify();//队列从满到不满,唤醒队列
return val;
}
}
这是一个相互唤醒的过程。
注意:在这个图中,如果使用if来判断,wait调用后,保不齐在别的线程中调用一个interrupt方法就直接唤醒线程了,所以应该使用while循环判断
本文到这里就结束了,下文将介绍多线程开发中常用到的定时器模型和代码的模拟实现,下期见