1.阻塞队列是什么
1、阻塞队列是一种特殊的队列,也遵守 "先进先出" 的原则.
2、阻塞队列能是一种线程安全的数据结构, 具有以下特性:
- 当队列满的时候, 继续入队列,就会出现阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列,也会出现阻塞, 直到有其他线程往队列中插入元素.
2.生产者消费者模型
- 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题.
- 生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯.
2.1 举例说明
就像包饺子这个工程,我们需要和面,擀饺子皮,包馅.
此时如果有三个人负责这个工程,并且只有一个擀面杖的时候,我们有下面两种分工:
1.每个人负责自己的,自己擀饺子皮并且擀完一张就包一个饺子[低效]
2.一个人专门负责擀饺子皮,两外两个负责包,这就是一个生产者消费者模型,此时,擀面皮的人就是生产者,包饺子的人,就是消费者,而放饺子皮的工具,就是交易场所.[高效]
2.2 优势
2.2.1 解耦合
- 解耦合,就是"降低模块之间的耦合"(模块之间的联系越强,耦合性越高)
首先看一个分布式的系统:
- 此时如果你想进行充值,A会直接把请求发给B,这时A和B直接的耦合就比较明显.
- 如果B服务器挂了,就会对A造成很大的影响,反之亦然.
- 并且如果要再添加一个服务器C,就需要对A的代码进行较大的改动.
此时引入生产者消费者模型,引入一个阻塞队列,就能有效解决上述问题:
- 此时,A和B通过阻塞队列进行交互,就可以很好的解耦合了.
- 如果A或B挂了, 由于它们之间没有直接的交互,所以没有太太影响.
- 并且如果要新增一个服务器C,A服务器也完全不需要任何修改,让C从队列中取元素即可.
2.2.2 削峰填谷
- 一台服务器,同一时刻能处理的请求数量是有上限的,每处理一个请求,都需要消耗一定的硬件资源,不同服务器配置不同(提供的硬件资源不同),处理的任务不同(每个请求消耗的资源不同),承担的上限也不同.(就像我们学校的教务系统,一抢课就崩溃)
- 一个分布式系统中,就经常出现,有的服务器承担的压力更大,有的更小
下面看一个分布式的系统:
- 此时A每次受到一个请求,B就需要立即处理一个请求,这就可能会导致B先挂了.
引入阻塞队列:
![]()
- 这时,当外界的请求突然暴涨,A收到的请求多了,A就会给队列写入更多的数据,但是B仍然可以按照既定的节奏来处理请求,不至于挂掉
- 这个阻塞队列就起到了一个缓冲的作用,把B本来要承受的压力给承受了(削峰)
- 当然,峰值只是暂时的,当峰值消退后,A收到的请求少了,B还是按照既定的节奏来处理请求,不至于太空闲(填谷)
举个典型的例子:
三峡大坝:
当上游水量激增,大坝就把洪峰给拦下来,保证下游按照一个比较缓和的速度放水(削峰).
当上游水量锐减,大坝就可以开闸放水,保证下游的用水可以正常供应(填谷).
3.消息队列
- 从上述叙述中可以看出,生产者消费者模型的重要性.虽然阻塞队列只是一个数据结构,但是可以把这个数据结构单独实现一个服务器程序,并且使用单独的主机/主机集群来部署,此时,这个所谓的阻塞队列,就进化成了"消息队列"
- 消息队列是在阻塞队列的基础上增加了“消息的类型”,并按照指定类型进行先进先出.
4.使用阻塞队列
Java标准库提供了阻塞队列的实现,下面我们基于阻塞队列,写一个简单的生产者消费者模型:
//生产者消费者模型
public class demo2 {
public static void main(String[] args) {
//阻塞队列,作为交易场所
BlockingDeque<Integer> queue = new LinkedBlockingDeque<>();
//负责生产元素
Thread t1 = new Thread(() ->{
int count = 0;
while (true){
try {
queue.put(count);
System.out.println("生产元素: "+ count);
count++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//负责消费元素
Thread t2 = new Thread(() ->{
while (true){
try {
Integer n = queue.take();
System.out.println("消费元素: "+ n);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
运行结果如下:
注意:
- 阻塞队列的实现可以基于链表,基于堆,基于数组.
- ArrayBlockingQueue的速度要更快,但前提是要知道最多有多少个元素(频繁扩容的开销较大).
- 如果不知道有元素有多少,使用LinkedBlockingQueue更合适.
5.实现阻塞队列
基于数组实现一个循环队列,来实现阻塞队列
循环队列的实现在数据结构专栏已详细介绍,这里不过多解释,稍加更改补充.
5.1 循环队列
在这里我们使用"用一个单独的变量来表示当前元素个数"的方法来判断队列是否满了
class MyBlockingQueue {
// 使用一个 String 类型的数组来保存元素. 假设这里只存 String.
private String[] items = new String[1000];
private int head = 0;
// 指向队列的头部
private int tail = 0;
// 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
// 当 head 和 tail 相等(重合), 相当于空的队列.
private int size = 0;
// 使用 size 来表示元素个数.
// 入队列
public void put(String elem) throws InterruptedException {
if(size >= items.length) {
// 队列满了.
return;
}
items[tail] = elem;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
}
// 出队列
public String take() throws InterruptedException {
if (size == 0) {
// 队列为空, 暂时不能出队列.
return null;
}
String elem = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
return elem;
}
}
在这里使用了下图所示代码来进行tail到达末尾就能回到开头的操作.
之前,我们是采用 tail=(tail+1)% item.length的操作,现在采用上述代码,有两个好处:
1.让写出来的代码,开发效率更高(好读,好理解,好修改)
2.让写出来的代码,运行效率更高(执行速度快)
5.2 改造成阻塞队列
5.2.1 线程安全
5.2.1.1 加锁
class MyBlockingQueue{
//使用一个String类型的数组来保存元素
private String[] items = new String[1000];
private int head = 0;//指向队列头部
private int tail = 0;//指向队列尾部的下一个元素
//队列中有效元素的范围[head,tail)
//当head和tail相等(重合),相当于空的队列
//使用size表示元素个数
private int size = 0;
//入队列
public void put(String elem){
//相当于直接把synchronized写到方法上
synchronized (this){
if (size >= items.length){
//队列满了
return;
}
items[tail] = elem;
tail++;
if (tail >= items.length){
tail = 0;
}
size++;
}
}
//出队列
public String take(){
synchronized (this){
if (size == 0){
//队列为空
return null;
}
String elem = items[head];
head++;
if (head >= items.length){
head = 0;
}
size--;
return elem;
}
}
}
5.2.1.2 内存可见性
class MyBlockingQueue{
//使用一个String类型的数组来保存元素
private String[] items = new String[1000];
volatile private int head = 0;//指向队列头部
volatile private int tail = 0;//指向队列尾部的下一个元素
//队列中有效元素的范围[head,tail)
//当head和tail相等(重合),相当于空的队列
//使用size表示元素个数
volatile private int size = 0;
//入队列
public void put(String elem){
//相当于直接把synchronized写到方法上
synchronized (this){
if (size >= items.length){
//队列满了
return;
}
items[tail] = elem;
tail++;
if (tail >= items.length){
tail = 0;
}
size++;
}
}
//出队列
public String take(){
synchronized (this){
if (size == 0){
//队列为空
return null;
}
String elem = items[head];
head++;
if (head >= items.length){
head = 0;
}
size--;
return elem;
}
}
}
5.2.2 实现阻塞
a)当队列满的时候,再进行put就会产生阻塞
b)当队列空的时候,再进行take就会产生阻塞
class MyBlockingQueue{
//使用一个String类型的数组来保存元素
private String[] items = new String[1000];
volatile private int head = 0;//指向队列头部
volatile private int tail = 0;//指向队列尾部的下一个元素
//队列中有效元素的范围[head,tail)
//当head和tail相等(重合),相当于空的队列
//使用size表示元素个数
volatile private int size = 0;
// 入队列
public void put(String elem) throws InterruptedException {
// 此处的写法就相当于直接把 synchronized 写到方法上了.
synchronized (locker) {
if (size >= items.length) {
// 队列满了.
// return;
locker.wait();
}
items[tail] = elem;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
// 用来唤醒队列为空的阻塞情况
locker.notify();
}
}
// 出队列
public String take() throws InterruptedException {
synchronized (locker) {
if (size == 0) {
// 队列为空, 暂时不能出队列.
// return null;
locker.wait();
}
String elem = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
// 使用这个 notify 来唤醒队列满的阻塞情况
locker.notify();
return elem;
}
}
}
下面思考一个问题,上述代码中满足条件就进行wait,那么当wait被唤醒之后,条件就一定满足了嘛??(比如put操作中的wait被唤醒了,那么此时队列就一定不满了嘛,有没有可能还是满的)
当然不是!!!
- 在当前的代码中,wait还可以被interrupt唤醒,此时直接会引起异常,不会继续执行
- 但是如果是按照try catch的方式来写,一旦是interrupt唤醒,此时代码往下走,进去catch,catch执行完毕,方法不会结束,继续向下执行,也就会触发"覆盖元素"逻辑.
那么怎么更改呢?
- wait被唤醒了之后,再次判定条件呗!!!此时条件如果还是队列满,就继续wait,如果不满,就可以继续执行了.
- 此处借助while循环的方式,巧妙地实现wait醒了之后,再次确认条件
最终完整代码如下:
class MyBlockingQueue {
// 使用一个 String 类型的数组来保存元素. 假设这里只存 String.
private String[] items = new String[1000];
// 指向队列的头部
volatile int head = 0;
// 指向队列的尾部的下一个元素. 总的来说, 队列中有效元素的范围 [head, tail)
// 当 head 和 tail 相等(重合), 相当于空的队列.
volatile private int tail = 0;
// 使用 size 来表示元素个数.
volatile private int size = 0;
private Object locker = new Object();
// 入队列
public void put(String elem) throws InterruptedException {
// 此处的写法就相当于直接把 synchronized 写到方法上了.
synchronized (locker) {
while (size >= items.length) {
// 队列满了.
// return;
locker.wait();
}
items[tail] = elem;
tail++;
if (tail >= items.length) {
tail = 0;
}
size++;
// 用来唤醒队列为空的阻塞情况
locker.notify();
}
}
// 出队列
public String take() throws InterruptedException {
synchronized (locker) {
while (size == 0) {
// 队列为空, 暂时不能出队列.
// return null;
locker.wait();
}
String elem = items[head];
head++;
if (head >= items.length) {
head = 0;
}
size--;
// 使用这个 notify 来唤醒队列满的阻塞情况
locker.notify();
return elem;
}
}
}
public class Demo20 {
public static void main(String[] args) throws InterruptedException {
// 创建两个线程, 表示生产者和消费者
MyBlockingQueue queue = new MyBlockingQueue();
Thread t1 = new Thread(() -> {
int count = 0;
while (true) {
try {
queue.put(count + "");
System.out.println("生产元素: " + count);
count++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(() -> {
while (true) {
try {
String count = queue.take();
System.out.println("消费元素: " + count);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
运行结果如下: