前言
本篇介绍阻塞队列,通过阻塞队列实现生产者消费者模型, 再通过模拟实现阻塞队列,再次实现生产者消费者模型;如有错误,请在评论区指正,让我们一起交流,共同进步!
文章目录
本文开始
1. 什么是阻塞队列?
阻塞队列:带有阻塞特性的队列
阻塞队列的特性:
① 如果队列为空,尝试出队列,会阻塞队列,等待到队列不为空为止;
② 如果队列满了,尝试入队列,会阻塞等待,等待到队列不满为止(不满了,就会入队);
【注】阻塞队列是线程安全的;
1.2 Java标准库提供的阻塞队列的使用
代码实现:
public static void main(String[] args) throws InterruptedException {
//BlockingDeque是一个接口,无法直接new实现,所以new 它的实现类:数组实现或者链表实现的阻塞队列
BlockingDeque<String> queue = new LinkedBlockingDeque<>();
//阻塞队列中的核心方法,注意两个:
//被阻塞,就可能会被中断提前唤醒 =》此时需要抛出异常
//put: 进队列
queue.put("h1");
queue.put("h2");
//take:出队列
String result = null;
result = queue.take();
System.out.println(result);
result = queue.take();
System.out.println(result);
}
1.3 多线程使用阻塞队列 - 生产者消费者模型
生产者消费者模型 就是 可以使用阻塞队列;
生产者和消费者之间,交互数据,需要一个交易场所,这个场所相当于一个 阻塞队列(安全又带有阻塞功能);
例如:当生产者生产的不够消费者使用时,阻塞队列会产生阻塞;当生产者生产大于消费时,阻塞队列也会阻塞;
【注】生产者生产的都放到阻塞队列中,消费者消费从阻塞队列中取;队列满和队列空都产生阻塞;
生产者消费者模型,目的是什么?解决什么问题呢?
在这之前先来认识一下,内聚与耦合;
① 耦合:描述 两个模块之间 的关联关系的强弱的;关联越强,耦合越高;
② 内聚:在 一个模块 中,模块内部之间相关联的强弱;
低内聚:相关联的代码,没放到一起(乱放),认为关联程度低;
高内聚:相关联代码,合理的分类,各部分之间关联程度高;
生产者消费者模型解决的问题:
1.可以让上下模块之间,进行更好的 “解耦合”
示例:
A服务器 调用 B服务器 =》A给B发送请求,B给A返回响应;
A与B之间直接通信,此时耦合就比较高,如果B挂掉, 对A也会有影响,A也会挂掉;
如果再加入C服务器,此时 对A也有一个较大的调整;此时A,B,C直接的关联较大,这也体现了高耦合;
引入生产者消费者模型的目的:为了解决耦合高的问题;
前提:A服务器不知道B的存在,B服务器不知道A的存在,但是A,B都知道阻塞队列;
【注】A,B,C可以算是业务服务器,需要随时修改,支持新的需求功能;阻塞队列与业务无关,代码变化程度低,会更稳定一些;
图示:
解耦合达到的效果:
B服务器挂掉,不会影响到A服务器;再添加C服务器,直接让C服务器从阻塞队列中获取请求即可,对A服务器的影响较小;
【注】阻塞队列服务器也会挂掉,但是概率很小;
2.削峰填谷
前提:
服务器收到的请求数量,都是与用户行为相关的;
有些情况下,用户请求会出现 “峰值” ,爆发式涨一波;
假设有A,B两个服务器,A,B直接调用(A收到请求,B马上接收请求),此时A,B都会有一个请求峰值;
例如:
A服务器平时接受每秒1w请求,突然某个时间点每秒收到3w请求;此时B也接收相同的请求,相对于A,B来说都会到达一个峰值,但是如果B服务器没有考虑到峰值情况,此时B服务器就可能挂掉;
【注】服务器为什么会挂掉?
服务器在接收每个请求的时候,都会消耗硬盘资源,包含cpu,内存,硬盘等;当某个硬件资源到达峰值,不能再给请求分配资源,此时服务器就会挂掉;
解决方式:引入生产者消费者模型;
图示:削峰填谷:用户请求多了用阻塞队列削峰,用户请求少了用阻塞队列填谷,使用之前堆积的数据;(绿色字体时解释)
1.4 实现生产者消费者模型
创建两个线程t1,t2;t1作为生产者,t2作为消费者;生产一个消费一个;
代码实现:
public static void main(String[] args) {
BlockingDeque<Integer> blockingDeque = new LinkedBlockingDeque<>();
//t1作为消费者
Thread t1 = new Thread( () -> {
//不断的从队列中取元素,使用循环
while (true) {
//从队列中取元素
try {
int value = blockingDeque.take();
System.out.println("消费元素:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
//t2作为生产者
Thread t2 = new Thread( () -> {
//生产元素
int value = 0;
while (true) {
try {
System.out.println("生产元素:" + value);
blockingDeque.put(value);
value++;
//防止生产太快,使用sleep阻塞一下
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t2.start();
}
代码结果:
1.5 模拟实现阻塞队列
1.实现普通队列
【注】BlockingQueue没有获取队列队首元素,不出队列(有peek的方法但不会阻塞);只要take去元素, put放元素;
代码实现:
class MyBlockingQueue {
private int[] items = new int[1000];
//队首head,队尾tail
private int head = 0;
private int tail = 0;
private int size = 0;
//入队列
public void put(int elem) {
//如果队列满了,插入失败
if(size == items.length) {
return;
}
items[tail] = elem;
tail++;
//万一tail到队列末尾,就需要让tail从头再来
if(tail == items.length) {
tail = 0;
}
//tail = tail % items.length;
size++;
}
//出队列
public Integer take() {
//如果对列为空,返回空
if(size == 0) {
return null;
}
//取出对首操作
int value = items[head];
head++;
//到最后一个位置
if(head == items.length) {
head = 0;
}
size--;
return value;
}
}
2.加上线程安全
根据上述,发现代码有很多修改操作,此时代码可能不是安全的;
为了保证线程安全:给每个方法加锁
多线程读的时候,size,head,value 可能发生内存可见性问题 - 加上volatile;
增加代码显示:
【注】这里的锁对象是this =》MyBlockingQueue ;
class MyBlockingQueue {
//其他一样
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
synchronized public void put(int elem) {
...
}
synchronized public Integer take() {
...
}
3.加上阻塞队列
①队列满了,就等待,等出队列操作里的唤醒操作;
②队列空了,就等待,等入队列操作里的唤醒;
代码实现:
【注】这里只显示增加的代码
class MyBlockingQueue {
//.....与上述代码一样
synchronized public void put(int elem) throws InterruptedException {
if(size == items.length) {
this.wait();
}
//....与上述代码一样
this.notify();
}
synchronized public Integer take() throws InterruptedException {
if(size == 0) {
this.wait();
}
//....与上述代码一样
this.notify();
return value;
}
}
1.5.1 模拟阻塞队列总代码
代码实现:
问题:到达队列末尾怎么在到头部?
① 重置tail = 0 ;② 使用 tail = tail % items.length ;
使用②这个求余代码的执行效率和开发效率都不会提高,一般不建议使用;
class MyBlockingQueue {
private int[] items = new int[1000];
//约定[head, tail]队列的有效元素
//队首head,队尾tail
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
//入队列
synchronized public void put(int elem) throws InterruptedException {
//如果队列满了,插入失败
if(size == items.length) {
this.wait();
// return;
}
items[tail] = elem;
tail++;
//万一tail到队列末尾,就需要让tail从头再来
if(tail == items.length) {
tail = 0;
}
size++;
this.notify();
}
//出队列
synchronized public Integer take() throws InterruptedException {
//如果对列为空,返回空
if(size == 0) {
this.wait();
//return null;
}
//取出对首操作
int value = items[head];
head++;
//到最后一个位置
if(head == items.length) {
head = 0;
}
size--;
this.notify();
return value;
}
}
产生Bug问题:
wait()方法可能被其他方法中断,例如interrupt方法;
此时它的等待条件还没满足,就提醒唤醒,此时可能产生bug;
解决方式:
使用循环判断两次:①在条件没满足的情况下,开始wait(); ②等到被唤醒,再次判断条件是否满足,不满足还可以继续wait();
给判定条件增加代码:
synchronized public void put(int elem) throws InterruptedException {
//...
while (size == items.length) {
this.wait();
}
//...
}
synchronized public Integer take() throws InterruptedException {
//...
while (size == 0) {
this.wait();
}
//...
}
1.5.2 更新版本模型实现阻塞队列实现生产者消费者模型代码
代码实现:
class MyBlockingQueue {
private int[] items = new int[1000];
//队首head,队尾tail
volatile private int head = 0;
volatile private int tail = 0;
volatile private int size = 0;
//入队列
synchronized public void put(int elem) throws InterruptedException {
//使用循环多次判断
while (size == items.length) {
this.wait();
}
items[tail] = elem;
tail++;
//万一tail到队列末尾,就需要让tail从头再来
if(tail == items.length) {
tail = 0;
}
size++;
this.notify();
}
//出队列
synchronized public Integer take() throws InterruptedException {
//如果对列为空,返回空
while (size == 0) {
this.wait();
//return null;
}
//取出对首操作
int value = items[head];
head++;
//到最后一个位置
if(head == items.length) {
head = 0;
}
size--;
this.notify();
return value;
}
}
public class ThreadDemo {
public static void main(String[] args) {
MyBlockingQueue myBlockingQueue = new MyBlockingQueue();
//消费者
Thread t1 = new Thread( () -> {
while (true) {
try {
int value = myBlockingQueue.take();
System.out.println("消费元素:" + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//生产者
Thread t2 = new Thread( () -> {
int value = 0;
while (true) {
try {
System.out.println("生产的元素" + value);
myBlockingQueue.put(value);
Thread.sleep(1000);
value++;
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
总结
✨✨✨各位读友,本篇分享到内容如果对你有帮助给个👍赞鼓励一下吧!!
感谢每一位一起走到这的伙伴,我们可以一起交流进步!!!一起加油吧!!!