大家好呀
我是浪前
今天给大家讲解的是**阻塞队列及生产者消费者模型**
祝愿所有点赞关注的人,身体健康,一夜暴富,升职加薪迎娶白富美!!!
点我领取迎娶白富美大礼包
🍎定义:
阻塞队列是多线程中的常用的数据结构
🍎特点:
阻塞队列是一种特殊的队列,具有的特点如下:
- 涉及到线程安全问题
- 带有阻塞特性
- 若队列为空,继续出队,会发生阻塞,阻塞到其他线程向队列中添加元素为止
- 若队列为满,继续入队,会发生阻塞,阻塞到其他线程向队列中取走元素为止
注意:
空队列出队阻塞,直到加元素为止
满队列入队阻塞,直到出元素为止
🍎阻塞队列的作用:
阻塞队列可以用来实现生产者消费者模型
生产者消费者模型是一种常见的多线程的代码的编写方式
🍎生产者消费模型
生产者和消费者借助于这个阻塞队列来进行交互
生产者把生产出来的内容放到阻塞队列中
消费者就从阻塞队列中获取内容
如果生产者生产得慢,那么消费者就得等
因为消费者从空的队列中获取元素就会阻塞
如果生产者生产得快,那么生产者就得等
因为生产者向满的队列中添加元素也会阻塞
作用:
阻塞队列可以降低资源竞争,提高执行效率
🍎阻塞队列的优势:
1.解耦合:
耦合:
两个模块的联系越紧密耦合度就越高
解耦合对于一个分布式系统来说是更加有意义的
客户端通过网络传递到服务器
分布式系统没有加阻塞队列之前:
如图:
上面的耦合度太高了
那么如何去降低这个耦合度?
我们就可以使用一个阻塞队列来降低这个耦合度
加了阻塞队列之后:
除了解耦合之外,阻塞队列还有一个优势:;
2.削峰填谷
峰: 短时间内请求量较多
谷: 短时间内请求量较少
没有加入阻塞队列的分布式:
加入了阻塞队列的分布式:
虽然加入了消息队列之后,会有一定的请求在消息队列中挤压
会使得A得到的响应速度会变慢,但是也好过完全没有响应
而且上述的高峰值情况不会一直出现,只会短时间出现
等到过了峰值之后 ,服务器A的请求量就恢复正常了
而服务器B就可以逐渐地把积压的数据都处理掉
上述的过程就是削峰填谷
销峰:当高峰值来了之后,我们通过队列把这个高峰值给抗住,保证服务器B不会一下子挂掉
填谷:当高峰过了之后,请求量没有那么多了之后,我们就让服务器B慢慢地处理掉积压在队列中的数据
作用:
通过消息队列让服务器之间的交互变得更加的灵活
即使两个服务器之间的步调不一致,也可以通过消息队列来协调
所以有了削峰填谷的机制之后,我们就可以保证在突发情况来临的时候,整个服务器系统仍然可以正确执行
就像是中国的三峡大坝一样
🍎阻塞队列的实现:
在我们的java标准库中已经提供了现成的阻塞队列让我们直接使用
public static void main(String[] args) {
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
}
Queue这里提供的各种方法
对于BlockingQueue来说也可以使用
但是一般不建议使用这些Queue中的方法
因为这些方法都不具备“阻塞”特性
所以我们就要使用专门带有阻塞特性的方法
put 方法:阻塞式的入队列
take 方法: 阻塞式的出队列
但是没有提供可以实现阻塞式的获取队首元素的方法
例子如下:
public class Text{
public static void main(String[] args) throws InterruptedException {
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
queue.put("111");
queue.put("123");
queue.put("233");
String elem = queue.take();
System.out.println(elem);
elem = queue.take();
System.out.println(elem);
elem = queue.take();
System.out.println(elem);
elem = queue.take();
System.out.println(elem);
}
}
代码运行结果:
通过刚刚的代码例子
我们了解了标准库中的阻塞队列如何使用
同时更重要的是我们还要能够自己去实现一个阻塞队列
阻塞队列的实现步骤:
一个普通的队列可以基于数组/链表
就是基于一个普通的队列,加上线程安全,加上阻塞,就可以了
🍎实现普通数组队列
基于数组实现的队列:
当队列初始化时:队列为空,head和tail重合的
当队列满了的时候:head和tail也是重合的
那么我们该如何去区分?
专门搞一个变量size来表示元素个数
size= 0则队列为空size为数组最大值则队列是满的
🍎初始化数组队列的代码:
代码如下:
class MyBlockingQueue{
private String[] data = new String[1000];
//队列的起始位置
private int head = 0;
//队列的结束位置的下一个位置
private int tail = 0;
//队列中的元素个数
private int size = 0;
//提供核心方法
//入队列:
public void put(String elem){
if(size == data.length){
return ;
}
data[tail] = elem;
tail++;
//如果tail++后到达了数组的末尾,此时就要让tail回到开头
if(tail == data.length){
tail = 0;
}
size++;
}
//出队列:
public String take(){
//队列为空直接返回
if(size == 0){
return null;
}
//删除队首元素,进行返回
String ret = data[head];
head++;
if(head == data.length){
return null;
}
size--;
return ret;
}
}
public class Demo06 {
public static void main(String[] args) {
}
}
上面是一个普通的由数组构成的队列
🍎实现阻塞队列
下面我们将在刚刚的代码上进行改进,修改为阻塞队列
这个队列引入线程安全,就要进行加锁:
加锁方式:
把入队的方法和出队的方法都全部加上锁:
代码如下:
class MyBlockingQueue{
private String[] data = new String[1000];
//队列的起始位置
private int head = 0;
//队列的结束位置的下一个位置
private int tail = 0;
//队列中的元素个数
private int size = 0;
//创建一个锁:
public final Object locker = new Object();
//提供核心方法
//入队列:
public void put(String elem){
//给put方法全加上锁:
synchronized (locker){
if(size == data.length){
return ;
}
data[tail] = elem;
tail++;
//如果tail++后到达了数组的末尾,此时就要让tail回到开头
if(tail == data.length){
tail = 0;
}
size++;
}
}
//出队列:
public String take(){
//给take方法全部加上锁:
synchronized (locker){
//队列为空直接返回
if(size == 0){
return null;
}
//删除队首元素,进行返回
String ret = data[head];
head++;
if(head == data.length){
return null;
}
size--;
return ret;
}
}
}
public class Demo06 {
public static void main(String[] args) {
}
}
阻塞的实现方式:
阻塞是使用wait和notify来实现:
🍎put方法中的阻塞:
在put方法中如果队列已经满了
此时若继续向队列中添加新的元素就会导致队列阻塞
此时我们就在put方法中使用wait方法来实现这个阻塞:
put方法中的阻塞实现:
if(size == data.length){
//如果是队列满,继续加入元素就会阻塞
locker.wait();
}
take方法中的notify来唤醒put方法中的wait阻塞:
put方法会一直阻塞到调用take方法为止
此时就可以在take方法中使用notify方法来唤醒:
//这个notify用来唤醒put方法中的wait方法
locker.notify();
🍎take方法中的阻塞:
在take方法中,如果队列已经为空了
此时继续删除元素就会发生阻塞
take方法中:
//队列为空,继续删除元素会阻塞
if(size == 0){
locker.wait();
}
此时take方法中的阻塞我们在put方法中进行唤醒
take方法中:
//这个notify方法用来唤醒take方法中的wait
locker.notify();
所有代码如下:
class MyBlockingQueue{
private String[] data = new String[1000];
//队列的起始位置
private int head = 0;
//队列的结束位置的下一个位置
private int tail = 0;
//队列中的元素个数
private int size = 0;
//创建一个锁:
public final Object locker = new Object();
//提供核心方法
//入队列:
public void put(String elem) throws InterruptedException {
//给put方法全加上锁:
synchronized (locker){
if(size == data.length){
//如果是队列满,继续加入元素就会阻塞
locker.wait();
}
data[tail] = elem;
tail++;
//如果tail++后到达了数组的末尾,此时就要让tail回到开头
if(tail == data.length){
tail = 0;
}
size++;
//这个notify方法用来唤醒take方法中的wait
locker.notify();
}
}
//出队列:
public String take() throws InterruptedException {
//给take方法全部加上锁:
synchronized (locker){
//队列为空,继续删除元素会阻塞
if(size == 0){
locker.wait();
}
//删除队首元素,进行返回
String ret = data[head];
head++;
if(head == data.length){
return null;
}
size--;
//这个notify用来唤醒put方法中的wait方法
locker.notify();
return ret;
}
}
}
put方法和take方法是相互唤醒的;
注意事项:
一个阻塞队列只有在要么是空,要么是满的时候才会出现阻塞,
所以take和put只有一边可以阻塞,
如果put阻塞了,其他线程继续调用put也都会阻塞,只能靠take唤醒
如果是take阻塞了,其他线程继续调用take也还是阻塞,只有靠put才能唤醒
当我们put方法因为队列满了,进入wait阻塞等待之后
一会儿wait返回,结束了wait的阻塞,此时的队列可能还是满的队列
因为wait除了notify之外,还有一个interrupt方法也是可以唤醒这个wait的
因为这个interrupt方法是可以中断wait的状态的
但是使用interrupt唤醒的时候会出现一个异常:InterrupttedException
在当前的代码中,如果是interrupt方法唤醒了wait,同时还使用了throws 抛出了异常
那么直接整个方法就结束了,不会出现问题。
但是若使用的是try catch来抛出异常就会出问题了
if(size == data.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
如果我们使用如上代码抛出异常,那么当出现异常的时候,方法就不会结束了
会继续向下执行下面的代码:
data[tail] = elem;
tail++;
if(tail == data.length){
tail = 0;
}
size++;
此时,就会把tail指向的元素给覆盖掉,实际上此处队列还是满着的
此时tail指向的元素并非是无效元素,那我们这里就是把一个有效元素给覆盖掉了
而且size加一之后也会比数组的最大长度还要大, 此时肯定是不合理的
🍎使用wait的注意事项:
我们使用wait的时候一定要注意:
有两种情况出现:
当wait被唤醒的时候,如果是通过notify来唤醒的
那么说明此时有其他线程调用了take方法
此时队列中已经不满了,就可以继续添加元素了
没有任何问题。
但是如果wait是通过Interrupt来唤醒的
那么此时队列还是满着的
就不可以添加元素了
如果继续添加元素,就会出问题
🍎如何解决:
解决原理:
如果本来是因为队列满了,wait从而进入了阻塞
那么在解除阻塞之后,还需要再去确认一次,看看队列是否还是满的
若队列还是满着的,说明没有调用take方法
刚刚的wait方法是通过Interrupt方法来唤醒的
此时就需要继续进行wait等待
代码实现:
在put方法中写出如下代码:
期望wait在被唤醒之后还去判定一次,看看队列满不满
如果队列不满就向后执行代码
如果队列还是满的,那么就继续阻塞等待
synchronized (this){
while (size == data.length){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
在take方法中写出如下代码:
while (size == 0){
this.wait();
}
我们直接在使用wait的时候,使用while作为条件判定的方式
目的就是为了让wait唤醒之后还能再确认一次, 是否条件仍然满足
如果条件还是满足就继续阻塞,不满足就跳出循环
同时我们还记得要给这些变量加上volatile关键字:
因为后面的代码中的这些变量里有的要进行读(判定),有的要进行写(修改)
加上volatile关键字为了防止内存可见性:
private volatile String[] data = new String[1000];
//队列的起始位置
private volatile int head = 0;
//队列的结束位置的下一个位置
private volatile int tail = 0;
//队列中的元素个数
private volatile int size = 0;
🍎阻塞队列完整代码:
完整的阻塞队列的实现代码如下:
public class Demo20 {
private volatile int tail = 0;
private volatile int size = 0;
private volatile int head = 0;
private volatile String[] data = new String[1000];
public void put(String elem) throws InterruptedException {
synchronized (this){
while (size == data.length){
this.wait();
}
data[tail] = elem;
tail++;
if(tail == data.length){
tail = 0;
}
size++;
this.notify(); //唤醒take的wait
}
}
public String take() throws InterruptedException {
synchronized (this){
while (size == 0){
this.wait();
}
String ret = data[head];
head++;
if(head == data.length){
head = 0;
}
size--;
this.notify();//唤醒put中的阻塞
return ret;
}
}
}
🍎生产者消费者模型的实现:
借助上述的阻塞队列,来实现一个简单的生产者消费者模型
实现步骤:
-
生产者和消费者都分别使用一个线程来表示(也可以使用多个线程来表示)
-
而且在生产者和消费者之间还需要一个阻塞队里来协调:
🍎生产者:
生产者:
//生产者
Thread t1 = new Thread(() ->{
});
实现细节:
- 使用while循环来向队列中添加元素
- 添加完毕之后sleep500毫秒
Thread t1 = new Thread(() ->{
int num = 1;
while(true){
try {
queue.put(num+"");
System.out.println("生产元素"+ num);
num++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
消费者:
//消费者
Thread t2 = new Thread(() ->{
});
实现细节:
使用循环向队列中删除元素
//消费者
Thread t2 = new Thread(() ->{
while(true){
try {
String result = queue.take();
System.out.println("消费元素" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
阻塞队列:
MyBlockingQueue queue = new MyBlockingQueue();
以上代码实现结果如下:
public class Demo06 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
//生产者
Thread t1 = new Thread(() ->{
int num = 1;
while(true){
try {
queue.put(num+"");
System.out.println("生产元素"+ num);
num++;
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//消费者
Thread t2 = new Thread(() ->{
while(true){
try {
String result = queue.take();
System.out.println("消费元素" + result);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
生产者每次生产一个元素就休眠500毫秒
生产者就生产得慢,消费者此时就将就着生产者
刚刚是生产者休眠500毫秒,下面是消费者休眠500毫秒:
public class Demo06 {
public static void main(String[] args) {
MyBlockingQueue queue = new MyBlockingQueue();
//生产者
Thread t1 = new Thread(() ->{
int num = 1;
while(true){
try {
queue.put(num+"");
System.out.println("生产元素"+ num);
num++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//消费者
Thread t2 = new Thread(() ->{
while(true){
try {
String result = queue.take();
System.out.println("消费元素" + result);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
结果如下: