目录
一 . 什么是阻塞队列?
二. 阻塞队列的优点
三. 实现阻塞队列
特点 : 先进先出
相比于普通队列 , 阻塞队列又有一些其他方面的功能 :
1 . 线程安全
2 . 产生阻塞效果
1) 如果队列为空 , 尝试出队列 , 就会出现阻塞 , 阻塞到队列不为空为止.
2) 如果队列为满 , 尝试入队列 , 就会出现阻塞 , 阻塞到队列不为满为止.
运用阻塞队列 , 就可以实现"生产者消费者模型"
举个栗子 :
过年时 , 家里都要包饺子 , 准备好馅 与 面 , 擀面杖 , 然后大家都围在一个桌子上.
一般就是一个人来负责专门用擀面杖来擀饺子皮 , 剩下的人负责包.
如果说包饺子的人比较慢 , 擀的饺子皮都放了一桌了 , 那么擀饺子皮的人就可以休息一会.
如果说擀饺子皮的人 , 擀的比较慢, 跟不上包饺子的人的速度了 , 那么休息的就是包饺子的人.
这就对应到了我们的生产者消费者模型 , 擀饺子皮的人就是生产者 , 包饺子的人就是消费者 , 休息意味着进入阻塞. 桌子就是相当于队列,擀饺子皮的人往里面放 , 包子饺子皮的人从里面拿.
Java标准库中的阻塞队列 : BlockingQueue
public class Demo1 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue queue = new LinkedBlockingDeque<>();
// //指定capacity 容量的大小
// BlockingQueue queue1 = new ArrayBlockingQueue<>(20);
queue.put(12);
queue.put(144);
int n = queue.take();
System.out.println(n);
}
}
假设有两个服务器 A B , A作为入口服务器用来接收用户的网络请求 , B作为应用服务器 , 用来给A提供一些数据~~
在上述这种情况下 , A 与 B 的耦合性是非常高的 !!
在开发A的时候就要了解B提供的一些接口.
在开发B的时候也要充分了解到A是怎么调用的.
在这种情况下 , 如果想把B换做C, 那么A的代码就要进行一些改动
或者说如果B服务器挂了 , 也就有可能导致A服务器也顺带挂了.
根据上述这些问题 , 我们就可以采用"生产者消费者模型" .
两大优点 :
1 . 能够让多个服务器程序之间更充分的解耦合
2 . 能够对于请求进行"削峰填谷"
给A 与 B 之间加上了阻塞队列 .
这时 , A 与 B 对于对方的存在都是不知晓的.
如果再要加服务器的话 , 直接跟阻塞队列交互就可以了 .
这样就实现了两个模块之间低耦合
未使用生产者消费者模型的时候 , 如果请求量突然暴涨.
由于A 和 B 直接调用的关系 , A收到了请求峰值, B也会同样有这个峰值.
假设A突然在某一时间收到的请求是平常的3倍.
A 作为入口服务器 , 计算量很轻, 请求量暴涨, 问题不大.
B 作为应用服务器 , 计算量可能很大 , 需要的系统资源也更多 , 服务器处理每个请求,都需要消耗硬件资源, 包括不限于(CPU , 内存 , 硬盘 , 带宽...) , 如果某个硬件资源达到瓶颈 , 此时服务器就挂了, 很有可能A服务器也就跟着挂了, 这给系统的稳定性带来了风险!
故如果使用生产者消费者模型则会更好 :
第二个优点 "削峰填谷"
"削峰" : A 收到的请求多了 , 给到阻塞队列 , 队列里的元素也多了 .
此时B仍然可以按照之前的速率来取元素. 队列帮B承担了压力.
"填谷" : 等这波请求峰值过去后, B 服务器仍然可以按照原有的速率处理队列中之前挤压的数据.
实现阻塞队列分三步 :
1 . 先实现一个普通队列
2 . 加上线程安全 (synchronized)
3 . 加上阻塞功能 (wait notify)
package Demo2;
public class MyBlockingQueue {
int[] arr = new int[20];
//记录数组中的有效数字个数
volatile int usedSize = 0;
volatile int head = 0;
volatile int tail = 0;
// put方法
synchronized public void put(int value) throws InterruptedException {
//如果队列满 则进入阻塞状态等待
if (arr.length == usedSize) {
this.wait();
}
arr[tail] = value;
tail++;
if (tail >= arr.length) {
tail = 0;
}
usedSize++;
// 放入成功后 调用notify 唤醒阻塞状态下的take
this.notify();
}
//take 方法
synchronized public int take() throws InterruptedException {
// 如果队列为空 则进入阻塞状态
if (usedSize == 0) {
this.wait();
}
int del = arr[head];
head++;
if (head >= arr.length) {
head = 0;
}
usedSize--;
// 唤醒put方法
this.notify();
return del;
}
}
给 put 和 take 方法都加进行加锁操作 , 保证线程安全.
并且在队列为空 , 或者为满的情况下让线程进入阻塞状态.
每次放入或者弹出一个元素的时候 , 去唤醒另一个线程(避免另一个线程是处于阻塞状态)
在上述代码中 , 还少思考的一点 , 如果在代码复杂的情况下 :
wait 是可能被其他方法给中断的 (interrupt 方法)
此时wait其实等待的条件还没成熟了 , 就被提前唤醒了!
因此代码就可能不符合预期了.
package Demo2;
public class MyBlockingQueue {
int[] arr = new int[2];
//记录数组中的有效数字个数
volatile int usedSize = 0;
volatile int head = 0;
volatile int tail = 0;
// put方法
synchronized public void put(int value) throws InterruptedException {
//如果队列满 则进入阻塞状态等待
while (arr.length == usedSize) {
this.wait();
}
arr[tail] = value;
tail++;
if (tail >= arr.length) {
tail = 0;
}
usedSize++;
// 放入成功后 调用notify 唤醒阻塞状态下的take
this.notify();
}
//take 方法
synchronized public int take() throws InterruptedException {
// 如果队列为空 则进入阻塞状态
while (usedSize == 0) {
this.wait();
}
int del = arr[head];
head++;
if (head >= arr.length) {
head = 0;
}
usedSize--;
// 唤醒put方法
this.notify();
return del;
}
}
附 : 写代码一般追求高内聚 , 低耦合 , 避免代码牵一发而动全身.
高内聚 : 相关联的代码 , 分门别类的规制起来,找起来容易.
低耦合 : 耦合两个模块之间的关联关系是强还是弱 , 关联越强,耦合越高.