堵塞队列呢是一种更为复杂的队列,他对比简单的队列有两个特性:1)线程安全;2)堵塞:a)队列为空时,尝试出队列,出队列操作就会堵塞,直到有新的元素添加进来为止;b)队列满了时,尝试入队列操作时会发生堵塞,直至队列被取走为止。
生产者消费者模型
堵塞队列呢有一个非常主要的场景:实现"生产者消费者模型"。什么是生产者消费者模型呢?
就拿这个服务器举例:这里直接A服务器请求B服务器,B服务器响应A服务器这种就不是生产者消费者模型。
当我们加入一个中转栈的时候就是实现了生产者消费者模型了,当A服务器想要去请求B服务器时,A服务器直接在中间栈中取即可,B服务器的响应也是直接发送给中间栈即可,这个中间栈呢也是我们今天要学的------堵塞队列
生产者消费者模型的优缺点
1.解耦合
通过上述服务器与服务器之间呢我们可以发现,当我们需要更改服务器A/B时,服务器B/A也会受到影响,这时这两个服务器就是高耦合了,但如果加入堵塞队列,我们更改某个服务器的代码时就不需要管另一个服务器的业务了,他们之间通过了堵塞队列来连接,而堵塞队列里的逻辑也不会有这两个服务器业务复杂。
2.削峰填谷
我们上过大学都知道,每当抢课时学校的官网都会崩溃,这就是因为进入官网的人太多了,所以A服务器一般都会有某个时刻点进去的人多,但B服务器的响应又有限,所以当我们直接连接两个服务器时可能造成服务器崩溃。加入了堵塞队列后,当点击A服务器的人多了就会对堵塞队列请求的多,但B服务器还是以之前的速度响应,不会影响到B服务器,但趁着这个峰值过去了服务器B还是以之前的响应速度响应传给堵塞队列,故而等下次点击量波峰时可以有效的缓解。
3.缺点
1.使代码变得复杂
引入队列之后,整体的结果会变得更复杂,此时,就需要更多的机器来部署,生产环境的结果复杂,管理起来也复杂。
2.效率降低
不使用堵塞队列时,服务器与服务器之间的请求和响应都是直接的,引入了堵塞队列他们还得加载到队列中,当数量达到一定量时会影响效率。
堵塞队列的使用
BlockingQueue的介绍
堵塞队列的类是BlockingQueue:
通过观察我们发现,new时会给我们很多对象,这里我们就讨论三个:ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue;
ArrayBlockingQueue:这时一个基于数组实现的堵塞队列;
LinkedBlockingQueue:这是一个基于链表实现的堵塞队列;
PriorityBlockingQueue:这时基于优先级队列实现的堵塞队列;
BlockingQueue这个类呢我们点击去发现他是继承了Queue类的,使用当我们使用这个类时是可以当作普通类来使用的;但我们学BlockingQueue的目的是学习堵塞队列,故而我们就讲讲堵塞队列的使用。
BlockingQueue的使用

这样子我们就简单的创建了一个容量为100的堵塞队列。

当我们使用put方法时就是往堵塞队列中加入元素了,当队列满了在添加时就会堵塞等待。
弹出队列是take(),当队列为空时就会堵塞等待。
堵塞队列的使用就讲到这里,我们重点讲解堵塞队列的实现!!!!
堵塞队列的实现
堵塞队列的实现主要涉及到一下几个问题:
1)怎么插入元素
2)怎么取出元素
3)怎么实现堵塞
1.插入与删除元素
我们实现堵塞队列主要以数组的方式来实现,插入和删除元素可以定义一个头指针和尾指针,插入与删除就是头指针和尾指针加减。
2.怎么实现堵塞
我们之前学过wait和notify,这两个就可以很好的实现堵塞等待,当数组满了的时候就使用wait等待,等到有元素的删除即可发出notify来截至堵塞。空队列也是如此。
代码的实现
class MyBlockingQueue{
private String[] date = null;
private int head = 0;//头指针
private int tail = 0;//尾指针
private int size = 0;//元素个数
public MyBlockingQueue(int capacity) {
date = new String[capacity];
}
public void put(String elem){
if(size >= date.length){
//堵塞等待
}
date[tail] = elem;
tail++;
if(tail >= date.length){
tail = 0;
//因为队列是先进先出的,当tail大于或等于了数组长度时,
// 说明数组添加元素从头开始了,这里也不要害怕head没有走,因为有堵塞等待。
}
}
public String take(){
if(size == 0){
//队列为堵塞等待
}
String ret = date[head];
head++;
if(head >= date.length){
head =0;
//因为队列是先进先出的,当队列走到了尽头的时候数组,
// 因为put是从头开始来的,所以令head为0,
// 当然也不要担心head和tail撞见,因为有堵塞等待
}
size--;
return ret;
}
}
第一步,我们设计出了基本的框架,这时插入和删除已经完成,但还差堵塞等待:我们得考虑wtai和notify在哪里加入?
class MyBlockingQueue{
private String[] date = null;
private int head = 0;//头指针
private int tail = 0;//尾指针
private int size = 0;//元素个数
public MyBlockingQueue(int capacity) {
date = new String[capacity];
}
public void put(String elem) throws InterruptedException {
synchronized (this){
if(size >= date.length){
this.wait();
//堵塞等待
}
date[tail] = elem;
tail++;
if(tail >= date.length){
tail = 0;
//因为队列是先进先出的,当tail大于或等于了数组长度时,
// 说明数组添加元素从头开始了,这里也不要害怕head没有走,因为有堵塞等待。
}
size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this){
if(size == 0){
this.wait();
//队列为堵塞等待
}
String ret = date[head];
head++;
if(head >= date.length){
head =0;
//因为队列是先进先出的,当队列走到了尽头的时候数组,
// 因为put是从头开始来的,所以令head为0,
// 当然也不要担心head和tail撞见,因为有堵塞等待
}
size--;
this.notify();
return ret;
}
}
}
wait和notify呢可以加的也很简单,wait可以直接在 if(size == 0)和 if(size >= date.length)里边加,因为这里就是为了他们堵塞等待的,notify呢可以加载最后面,因为此时也快要解锁了让这两个方法能更好的竞争锁。
但这里还面临这一个问题,我们知道堵塞是可以通过其他方法唤醒的,比如Interrupt,当wait被其他方法唤醒时会出现队列满了/空了也会执行下去造成越界,而出现bug,所以我们可以把两个if改成while循环,此时即使wait提前被唤醒了也还会经过while判断是否成立,成立即退出,不成立则继续循环,故而最终代码如下:
class MyBlockingQueue {
private String[] data = null;
// 队首
private int head = 0;
// 队尾
private int tail = 0;
// 元素个数
private int size = 0;
public MyBlockingQueue(int capacity) {
data = new String[capacity];
}
public void put(String elem) throws InterruptedException {
synchronized (this) {
while (size >= data.length) {
// 队列满了. 需要阻塞的
// return;
this.wait();
}
data[tail] = elem;
tail++;
if (tail >= data.length) {
tail = 0;
}
// tail = (tail + 1) % data.length;
size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (size == 0) {
// 队列空了. 需要阻塞
// return null;
this.wait();
}
String ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
this.notify();
return ret;
}
}
}