什么是阻塞队列
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则,是一个线程安全的数据结构.
- 当队列满的时候,继续入队列就会收到阻塞,直到有其它的线程从队列中取走元素.
- 当队列为空的时候,继续出队列也会阻塞,直到有其它线程从队列中插入元素.
我们可以基于阻塞队列实现生产者消费者模型.
生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
阻塞队列在此处的作用:
1. 阻塞队列相当于一个缓冲区,平衡了生产者和消费者的处理能力.当生产者生产元素出来时,
直接扔给阻塞队列.
2. 阻塞队列使生产者和消费者之间解耦.消费者也是直接从阻塞队列中拿元素.
生产者消费者模型的优势:
- 解耦合: 降低了模块之间的耦合.
- 削峰填谷: 一台服务器在同一时刻处理的请求数量是有上限的.
此时有两个服务器A和B,假设A承受的压力更大,B承受的压力更小.
当请求突然大量增多时,B就可能会挂掉.
为了防止这种情况,A就可以把突然增多的请求写到阻塞队列中(B是用来处理A发来的请求的),
B就可以按照原来的节奏来处理请求,此时阻塞队列就起到了缓冲的作用,这就是"削峰".
当峰值消退的时候,A的请求少了,B也可以按照原来的节奏处理请求,B也不至于台空闲,这就是"填谷".
正是因为生产者消费者模型,所以我们会把阻塞队列单独实现成一个服务器程序 ,并使用单独的主机/主机集群来部署.
此时的阻塞队列,就进化为了"消息队列".
public static void main(String[] args) throws InterruptedException {
// offer和poll不带有阻塞功能
BlockingDeque<String> queue = new LinkedBlockingDeque<>(10);
// put和take带有阻塞功能
queue.put("hello");
String str = queue.take();
System.out.println("1: "+ str);
str = queue.take();
System.out.println("2" + str);
}
// 结果: 1: hello ....
// 我们取出hello时,阻塞队列中已经没有元素了,就会进入阻塞.
利用阻塞队列实现生产者消费者模型:
public static void main(String[] args) {
BlockingDeque<Integer> deque = new LinkedBlockingDeque<>(10);
// 负责生产元素
Thread thread1 = new Thread(() -> {
int count = 0;
while (true) {
try {
deque.put(count);
System.out.println("生产元素: " + count);
count++;
// 用1s时间生产元素
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
// 负责消费元素
Thread thread2 = new Thread(() -> {
while (true) {
try {
Integer n = deque.take();
System.out.println("消费元素: " + n);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread1.start();
thread2.start();
}
阻塞队列的模拟实现
在java标准库中已经提供了现成的阻塞队列的实现了.
1. 基于链表
2. 基于堆
3. 基于数组
下面我用数组模拟实现一个阻塞队列供参考:这里主要实现put和take方法
class MyBlockingQueue {
private String[] str = new String[1000];
// 阻塞队列头部
private int head = 0;
// 阻塞队列尾部
// 当head == tail的时候,队列为空
private int tail = 0; // [head, tail)
// 阻塞队列长度, 判断队列是否满
private int size = 0;
public void put(String s) throws InterruptedException {
if(this.size >= str.length) {
// 队列满了
return;
}
str[tail] = s;
tail++;
if(tail >= str.length) {
tail = 0;
}
this.size++;
}
public String take() {
if(this.size == 0) {
return null;
}
String elem = str[head];
head++;
if(head >= str.length) {
head = 0;
}
this.size--;
return elem;
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue deque = new MyBlockingQueue();
deque.put("hello");
deque.put("hehe");
String s = deque.take();
System.out.println("1: "+ s);
s = deque.take();
System.out.println("2: " + s);
}
}
可以看出,这个队列满足先进先出的规律,但是它还不是阻塞队列!
下面我将加入线程方面的知识把它改为阻塞队列.
class MyBlockingQueue {
private String[] str = new String[1000];
// 阻塞队列头部
volatile private int head = 0;
// 阻塞队列尾部
// 当head == tail的时候,队列为空
volatile private int tail = 0; // [head, tail)
// 阻塞队列长度, 判断队列是否满
volatile private int size = 0;
public void put(String s) throws InterruptedException {
synchronized (this) {
while (this.size >= str.length) {
// 队列满了
//return;
this.wait();
}
str[tail] = s;
tail++;
if(tail >= str.length) {
tail = 0;
}
this.size++;
this.notify();
}
}
public String take() throws InterruptedException {
synchronized (this) {
while (this.size == 0) {
//return null;
this.wait();
}
String elem = str[head];
head++;
if(head >= str.length) {
head = 0;
}
this.size--;
this.notify();
return elem;
}
}
}
public class Demo21 {
public static void main(String[] args) throws InterruptedException {
MyBlockingQueue deque = new MyBlockingQueue();
Thread thread1 = new Thread(() -> {
int count = 0;
while (true) {
try {
deque.put(count + " ");
System.out.println("生产元素: " + count);
count++;
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
Thread thread2 = new Thread(() -> {
int count = 0;
while (true) {
try {
String elem = deque.take();
System.out.println("消费元素: " + elem);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
thread1.start();
thread2.start();
}
}
- 线程安全: 给put和take方法加上锁,保证多线程调用的时候,保证线程安全.
还要保证内存可见性问题,给head,tail,size变量加上volitile. - 实现阻塞功能: 当队列满的时候,此时应进入阻塞等待.相同的,队列为空的时候,也应该进入阻塞等待.在这里把if变为while,是因为如果是interrupt的原因被唤醒后,阻塞队列可能还是满的,这个时候去执行下面的逻辑就发生错误了,使用while循环,保证唤醒后再判断一次队列是否满了,此时队列没满才真的被唤醒了.
结语
我在这篇博客中总结了阻塞队列相关知识点,以及模拟实现了put和take方法,我在下一遍中会模拟实现计时器.有问题的小伙伴可以评论区提出问题,望大家多多支持.