阻塞队列是一种特殊的队列,遵守 “先进先出” 的原则,并且是一种线程安全的数据结构。
阻塞队列的特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素。
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素。
阻塞队列的一个典型应用场景就是 “生产者消费者模型”。
以擀面皮+包饺子为例:
场景一:
假设有两个服务器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 生产一个元素.
// 让消费者则直接消费, 不受限制.
}
}
代码效果:生成者每生成一个元素,消费者消费一个元素,如果队列没有元素,消费者就阻塞等待生产者。
阻塞队列实际上就是循环队列,而循环队列有两种实现方式
用一个变量记录队列元素个数【使用这种方式实现】
浪费一个空间实现
力扣链接
假设数组长度为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;
}
}
由于入队和出队都有写操作,所以我们避免线程的不安全,进行上锁处理
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可能被其他方法唤醒(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;
}
}
情况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,所以很快就生产满了,之后就需要等着消费者每消费一个,才能生产一个。