目录
一、什么是阻塞队列
队列:先进先出
阻塞队列:就是带有阻塞特性的队列,主要概念如下:
1.如果队列为空,尝试出队列,就会阻塞等待,等待到队列不为空为止;
2.如果队列未满,尝试入队列,也会阻塞等待,等待到队列不满为止。
3.阻塞队列是线程安全的。
当我们写多线程代码时,多个线程之间进行数据交互,就可以使用阻塞队列简化代码编写。
二、使用阻塞队列
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
这里的BlockingDeque是一个接口,所以不能直接new。
阻塞队列的核心方法:
1.put入队列
这里入了五个元素。
queue.put("hello1");
queue.put("hello2");
queue.put("hello3");
queue.put("hello4");
queue.put("hello5");
2.出队列。
将前面五个元素出队列,当前面五个队列都出去了,队列为空了,此时队列发生阻塞。
//2.take出队列
String result = null;
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
result = queue.take();//发生阻塞,无法打印
System.out.println(result);
三、“生产者消费者模型” — 多线程使用阻塞队列模型
1.解释“生产者消费模型”
关于这个模型,举例说明如下:
假如有几个人在包饺子,这几个人发现,擀面杖只有一个,如果每个人边擀面边包饺子太慢,还要争抢擀面杖,于是他们就做了如下改动:
此时,我们就将1号比喻为生产者(产生资源),包饺子的另外几个人就是消费者(消耗资源),这个盖帘就是阻塞队列(放置资源)。
所以结合阻塞队列的特性:
盖帘放不下了(队列满了),1号就暂时不放了(阻塞等待,就暂时不入队列了);
盖帘空了(队列空了),包饺子的人就可以歇息(阻塞等待,暂时不出队列了)。
2.生产者消费模型解决的问题
(1).可以让上下游模块之间,进行更好的"解耦合"
高耦合:联系紧密,关系紧密,如果两者之间符合高耦合,乙方有什么变化,可能就会影响到另一方。低耦合则是与之相反。
内聚:就是指有关联的东西放在一起。
考虑到以下场景:
A服务器调用B服务器:A给B发送请求,B给A返回响应,两个服务器正在进行正常交互,两者之间属于高耦合的情况,如果A或者B出现问题,就会对双方出现影响。
然后此时来了一个C,也要于A进行交互,
此时A如果要与C进行交互,A就需要做一个很大的调整,但此时就会影响到B,针对以上的问题我们就需要引入“生产者消费者模型”,用到阻塞队列。
此时A,B,C相互不知道对方的存在,如果需要交互资源,直接通过阻塞队列服务器获取即可,相互不会受到影响,起到了一个“解耦合”的效果。
关于阻塞队列服务器,其实也有可能出问题,但是比ABC出现问题的概率要低,因为我们做业务,一般实在ABC里面进行,容易出bug,而不用阻塞队列,所以阻塞队列不容易出问题。
(2)削峰填谷
举例以下情况,用户发出请求,AB两个服务器是相互调用的关系,如下图,
峰值:比如A平时受到1万/秒的请求,然后突然出现了3万/秒的请求,这种情况就会出现峰值。
如果A出现峰值,那么B也会出现峰值。如果此时B没有考虑峰值的处理,由于服务器处理每个请求,都要消耗一定的硬件资源(CPU,内存......),那么这个B就会出现问题,这种情况,就会给系统的稳定性带来一定的风险。
所以以上直接调用的方式就会给系统稳定性带来风险,所以就需要运用“阻塞队列”进行“削峰填谷”。
如下图,
虽然A收到的请求多了,但由于每次是A,B之间不是直接交互,有了阻塞队列,就是阻塞队列里面的元素增多了,但是B还是按照原来的速率从阻塞队列里拿取请求,不会受到影响,阻塞队列帮助B承担了压力,此过程即为“削峰”。
当峰值过去之后,还有一个波谷,但是由于B有了阻塞队列,也就不用改变畜栏里请求的速率,仍按照原来的速率消耗阻塞队列中的元素,此过程即为“填谷”。
(3)."生产者消费者模型" —— 阻塞队列代码实现
代码作用:
生产者:每隔1s产生一个元素
消费者:直接消费,不受限制。
代码实现如下:
public static void main(String[] args) {
//创建一个阻塞队列
BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
//消费者
Thread t1 = new Thread(() -> {
while(true) {
int value = 0;
try {
value = blockingDeque.take();
System.out.println("消费元素:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
//生产者
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
System.out.println("生产元素:" +value);
try {
blockingDeque.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
四、实现一个自己的阻塞队列(基于数组实现)
三步实现阻塞队列:
1.实现一个队列
队列空:head和tail重合
队列满:当tail < head时,就视为队列满
2.加上线程安全
基于队列有很多的读入和修改操作,此时我们需要使用synchronized和volatile关键字。
3.加上阻塞功能
阻塞机制:
(1)队列满了就wait(),然后队列有元素出去了,不满了,就让notify()唤醒;
(2)队列空了就wait(),当队列增加了元素也让notify()唤醒。
此处有一个注意点,wait()有可能会被提前唤醒,很多方法,比如使用interrupt,就会把wait()提前还行,但是此时可能条件还没满足(还没满或者还没非空),wait就直接被唤醒往下走了,就有可能会出现问题。
所以我们就需要在wait唤醒以后,再加一个判定条件:
wait()之前,发现条件不满足,开始wait();然后等到wait()被唤醒了以后,再确认以下这个条件是不是满足的,如果不满足,还是再继续wait()
编写阻塞队列代码
class MyBlockingQueue {
private int[] items = new int[1000];
//约定[head,tail]队列的有效元素
volatile private int head = 0;//指向队首元素的下标
volatile private int tail = 0;//指向队尾元素的下标
volatile private int size = 0;//获取队列种的元素个数
//入队列
synchronized public void put(int elem) throws InterruptedException {
while (size == items.length) {
//队列满了,插入失败
this.wait();//队列满了就要阻塞
}
//把新元素放到tail所在的位置上
items[tail] = elem;
tail++;
//万一tail达到末尾,就需要让tail从头再来,达到一个循环的效果
if (tail == items.length) {
tail = 0;
}
//tail = tail % items.length;//求模也可以表示循环
size++;
this.notify();//唤醒空的队列
}
//出队列
synchronized public Integer take() throws InterruptedException {
while (size == 0) {
//return null;
this.wait();//队列空了就要阻塞
}
int value = items[head];
head++;
if (head == items.length) {
head = 0;
}
size--;
this.notify();//唤醒满的队列
return value;
}
}
阻塞队列到这里就结束了,求一键三连啦~~