文章目录
- 阻塞队列
- 一、认识阻塞队列
- 1.什么是阻塞队列
- 2.生产者消费者模型
- 3.为什么要使用阻塞队列
- 二、实现生产者消费者模型
- 三、实现阻塞队列
- 1.实现循环队列
- 2.实现阻塞队列
- 3.测试阻塞队列
阻塞队列
一、认识阻塞队列
1.什么是阻塞队列
阻塞队列是一种特殊的队列,遵守 “先进先出” 的原则,并且是一种线程安全的数据结构。
阻塞队列的特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素。
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素。
阻塞队列的一个典型应用场景就是 “生产者消费者模型”。
2.生产者消费者模型
以擀面皮+包饺子为例:
- 每个人独立完成擀面皮+包饺子的全流程,假设只有一个擀面杖,但是有多个人要包饺子,这样就会存在有人等待擀面杖的情况,导致效率下降。
- 一个人专门负责擀饺子皮, 另外的人负责包, 擀饺子的人每次擀好一个皮, 就放到装饺子皮的某一件东西上面, 其他人直接取饺子皮进行包饺子。【生产者:擀饺子皮的人;消费者:包饺子的人】
- 如果面皮已经有很多了,擀面皮的人就等一会包饺子的人;如果面皮不够用,那么包饺子的人就等一会擀面皮的人。
3.为什么要使用阻塞队列
场景一:
假设有两个服务器A(请求服务器),B(应用服务器),如果A,B直接传递消息,而不通过阻塞队列,那么当A请求突然暴涨的时候,B服务器的请求也会跟着暴涨,由于B服务器是应用服务器,处理的任务是重量级的,所以该情况B服务器大概率会挂。
场景二:
如果使用生产者消费者模型,那么即使A请求暴涨,也不会影响到B,顶多A挂了,应用服务器不会受到影响,这是因为A请求暴涨后,用户的请求都被打包到阻塞队列中,B还是以相同的速度处理这些请求,所以生产者消费者模型可以起到削峰填谷
的作用。
削峰填谷
把请求高峰部分削掉,填补到请求低谷部分,从而使整个过程看起来趋于平缓
二、实现生产者消费者模型
基于标准库的阻塞队列简单实现的生产者消费者模型
public class ThreadDemo20 {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<>();
// 消费者
Thread t1 = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
// 生产者
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产元素: " + value);
blockingQueue.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
// 上述代码, 让生产者, 每隔 1s 生产一个元素.
// 让消费者则直接消费, 不受限制.
}
}
代码效果:生成者每生成一个元素,消费者消费一个元素,如果队列没有元素,消费者就阻塞等待生产者。
三、实现阻塞队列
阻塞队列实际上就是循环队列,而循环队列有两种实现方式
- 用一个变量记录队列元素个数【使用这种方式实现】
- 浪费一个空间实现
1.实现循环队列
假设数组长度为8,size=0表示队列为空,size=8表示队列满
插入元素
删除元素
如果rear到了最后一个位置,插入了元素之后,rear就应该指向第一个位置,所以不能一味的用rear++和font++
应该使用**(rear+1)%数组长度和(front+1)%数组长度**
代码实现
//循环队列
public class MyCircularQueue {
//队列数据
private int[] elems ;
//队头指针
private int front;
//队尾指针
private int rear;
//队列元素个数
private int size;
public MyCircularQueue(int k) {
elems = new int[k];
}
//出队
public boolean deQueue() {
if (isEmpty()) {
//队列为空
return false;
}
int ret = elems[front];
front =(front+1)%elems.length;
size--;
return true;
}
//入队
public boolean enQueue(int elem) {
if (isFull()) {
//队列满
return false;
}
elems[rear] = elem;
rear = (rear+1)%elems.length;
size++;
return true;
}
//获取队头元素
public int Front() {
if(isEmpty()) {
return -1;
}
return elems[front];
}
//获取对尾元素
public int Rear() {
if(isEmpty()) {
return -1;
}
int index = (rear == 0) ? elems.length-1 : rear-1;
return elems[index];
}
//是否为空队列
public boolean isEmpty() {
return size==0;
}
//是否满队列
public boolean isFull() {
if( size == elems.length) {
return true;
}
return false;
}
}
2.实现阻塞队列
- 由于入队和出队都有写操作,所以我们避免线程的不安全,进行上锁处理
synchronized public Integer take() {
}
//入队
synchronized public void put(int val) {
}
- put和take两个方法都会读取变量,所以我们用volatile修饰变量,避免内存可见性
volatile private int front = 0;
volatile private int rear = 0;
volatile private int size = 0;
- 入队时,队列为满需要使用
wait
方法使线程阻塞,直到有旧元素出队才使用notify
通知线程执行。
出队时,队列为空需要使用wait
方法使线程阻塞,直到有新元素入队才使用notify
通知线程执行。
// 入队列
synchronized public void put(int elem) throws InterruptedException {
while (size == items.length) {
// 队列满了, 阻塞等待
this.wait();
}
items[rear] = elem;
rear = (rear+1) % items.length;
size++;
//唤醒出队列的wait
this.notify();
}
// 出队列
synchronized public Integer take() throws InterruptedException {
while (size == 0) {
// 队列空了, 阻塞等待.
this.wait();
}
int value = items[front];
front = (front+1) % items.length;
size--;
//唤醒入队列的wait
this.notify();
return value;
}
- 入队和出队的wait不会同时发生
- 即使都没有wait,执行了notify并不会产生影响,类似投篮投了但没有进。
- 使用 while (size == items.length) 而不是直接if判断:
- wait可能被其他方法唤醒(interrupt),导致程序无法正常运行
- 解决办法:在wait唤醒之后,再确认一下条件是否满足。(比如入队列的wait,被唤醒后再次判断队列是否满,如果是满了,就继续wait)
最终版
class MyBlockingQueue {
private int[] items = new int[1000];
volatile private int front = 0;
volatile private int rear = 0;
volatile private int size = 0;
// 入队列
synchronized public void put(int elem) throws InterruptedException {
while (size == items.length) {
// 队列满了, 阻塞等待
this.wait();
}
items[rear] = elem;
rear = (rear+1) % items.length;
size++;
//唤醒出队列的wait
this.notify();
}
// 出队列
synchronized public Integer take() throws InterruptedException {
while (size == 0) {
// 队列空了, 阻塞等待.
this.wait();
}
int value = items[front];
front = (front+1) % items.length;
size--;
//唤醒入队列的wait
this.notify();
return value;
}
}
3.测试阻塞队列
情况1:生产者生产与消费者消费的频率一致
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
// 消费者 每1s消费一个
Thread t1 = new Thread(() -> {
while (true) {
try {
int value = queue.take();
System.out.println("消费: " + value);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 生产者 每1s生产一个
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产: " + value);
queue.put(value);
value++;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
运行结果:
情况2:生产者生产频率比消费者消费的频率更快
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
// 消费者
Thread t1 = new Thread(() -> {
while (true) {
try {
int value = queue.take();
System.out.println("消费: " + value);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 生产者
Thread t2 = new Thread(() -> {
int value = 0;
while (true) {
try {
System.out.println("生产: " + value);
queue.put(value);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
运行结果:
由于生产者没有sleep,所以很快就生产满了,之后就需要等着消费者每消费一个,才能生产一个。