🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!
人生格言:当你的才华撑不起你的野心的时候,你就应该静下心来学习!欢迎志同道合的朋友一起加油喔🦾🦾🦾
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个🐒嘿嘿
谢谢你这么帅气美丽还给我点赞!比个心
目录
一.什么是阻塞队列
二.使用阻塞队列/生产者消费者模型的好处
三.阻塞队列/生产者消费者模型的简单使用
四. 模拟实现阻塞队列
一.什么是阻塞队列
阻塞队列(BlockingQueue) 是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
1.当进行入队操作的时候,队列为满,入队操作就阻塞,直到队列非满的时候入队操作才完成
2.当进行出队操作的时候,队列为空,出队操作就阻塞,直到队列非空的时候出队操作才完成
生产者消费者模型的生活案例:
例如:两个人包饺子,其中有一个人需要生产饺子皮,他就是生产者,另外的一个人就是消费者,而生产者生产的饺子皮放在桌子上,桌子就是"交易场所".如果生产者生产过快饺子皮已经放满了桌子,他就能进行阻塞等待,如果是饺子皮的生产速度慢于包饺子的速度,那消费者就能够进行阻塞等待
二.使用阻塞队列/生产者消费者模型的好处
生产者消费者是一种高内聚,低耦合的模型,这也是它的优势,特别是在服务器场景中,假设有两个服务器A(请求服务器),B(应用服务器),如果A,B直接传递消息,而不通过阻塞队列,那么当A请求突然暴涨的时候,B服务器的请求也会跟着暴涨,由于B服务器是应用服务器,处理的任务是重量级的,所以该情况B服务器大概率会挂。
但是,如果使用生产者消费者模型,那么即使A请求暴涨,也不会影响到B,顶多A挂了,应用服务器不会受到影响,这是因为A请求暴涨后,用户的请求都被打包到阻塞队列中(如果阻塞队列有界,则会引起队列阻塞,不会影响到B),B还是以相同的速度处理这些请求,所以生产者消费者模型可以起到“削峰填谷”的作用。
三.阻塞队列/生产者消费者模型的简单使用
public class ThreadDemo1 {
public static void main(String[] args) {
BlockingQueue<Integer> blockingQueue =new LinkedBlockingDeque<>();
//消费者
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();
}
}
四. 模拟实现阻塞队列
1. 实现一个阻塞队列(这里采用顺序存储的环形队列)
2. 保证线程安全
3. 实现阻塞效果
class MyBlockingQueue {
private int[] items =new int[1000];
private volatile int head =0; //头部
private volatile int tail =0; //尾部
private volatile int size =0; //队列长度
//入队列
synchronized public void put(int elem) throws InterruptedException {
//此外为什么if换成while更合适,防止别的代码暗中调用interrupt方法,把wait提前唤醒了
//明明还不满足唤醒条件(队列没满才或者非空才会被唤醒) ,导致提前被唤醒就行执行下面代码,会出错抛异常
while (size == items.length) { //判断队列是否为满,满了则不能插入
this.wait(); //由take方法里的notify来唤醒
}
items[tail] = elem; //进行插入操作,将elem放到items里,放到tail所指向的位置
tail++;
if (tail == items.length) {
tail = 0;
}
size++;
this.notify();
}
//出队列,返回删除的元素内容
synchronized public int take() throws InterruptedException {
while (size == 0) { //判断队列是否为空,为空则不能出队
this.wait(); //由put方法里的notify来唤醒
}
int value = items[head]; //非空,取元素
head++;
if (head == items.length) {
head = 0;
}
size--;
this.notify();
return value;
}
}
public class ThreadDemo2 {
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);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
//生产者
Thread t2 =new Thread(() -> {
int value =0;
while (true) {
try {
System.out.println("生产:" + value);
queue.put(value++);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
}
为了保证线程安全,需要注意的事项
1. 修改操作要保证原子性, put和take方法都有修改数据的操作, 所以这两个方法都直接加锁, 用 synchronized 修饰
2. 读操作要保证满足内存可见性, 所以 size, head和 tail都加上 volatile 修饰为了满足阻塞队列的阻塞特性,需要在入队为满和出队为空时加wait方法进行阻塞等待,
而put 方法里判满时的 wait , 是由 take 方法最后的 notify 唤醒, take 里判空时的 wait , 是由 put方法最后的 notify 唤醒, put 和 take 不可能同时进入阻塞状态
wait方法是还有可能被外部的 interrupt 方法打断的,导致不是还没满足唤醒条件就继续执行下面代码,此时会出错抛异常,所以要把 if 换成 while , 如果不是被 notify 唤醒, 就再判断一下是否满足非空 / 非满这个条件