一.什么是阻塞队列
阻塞队列是在普通的先进先出队列的基础上进行了扩充
- 线程安全
- 具有阻塞性
a)如果队列没空进行出队列操作,此时就会出现阻塞;一直阻塞到其他线程往队列添加元素
b)如果队列为满进行入列操作,此时也会出现阻塞;一直阻塞到其他线程从队列取走元素
基于阻塞队列,最大的应用场景就是实现“生产者消费者模型”。
二.生产者消费者模型
1)生产者消费者模型是什么
以包饺子为例
假设有三个人A,B,C在桌前包饺子,包饺子有擀饺子皮,包饺子。
第一种典型包法,三个人,分别进行擀饺子皮和包饺子这两操作。
这种方法三个人需竞争擀面杖,涉及阻塞等待,较为低效。
第二种典型包法,三人分工合作
A负责擀饺子皮,BC负责包饺子。
此时,A是饺子皮的生产者,BC是饺子皮的消费者。
而用于存放擀好的饺子皮的桌子就是阻塞队列
生产者与消费者模型在后端发开中会经常涉及,当下后端开发,常用“分布式结构”,不是一台服务器解决所有问题,而是分成多个服务器,服务器之间互相调用。
通常谈到的“阻塞队列”是代码中的一个数据结构。
但由于这东西太好用了,以至于会把这样的数据结构单独封装成一个服务器程序。
并且在单独的服务器上进行部署
这样的阻塞队列有了一个新的名字“消息队列”(Message Queue,MQ)
2)生产者消费者模式的优点
使用生产者消费者模型有两方面好处:
1)服务器之间的“解耦合”(模块之间的关联程度)
如果是直接调用的关系
编写A代码中,就会出现许多B服务器相关的代码。
编写B代码中,也会出现许多A服务器相关代码。
如果A服务器挂了,可能B服务器也会受到影响,并且,若是后续想增加一个C服务器,对于A的改动就会较大。
2)通过中间的阻塞队列,可以起到"削峰填谷"效果。(在遇到请求量激增突发的情况下,可以有效保护下游服务器不会被冲垮)
如果是直接调用,A收到多少请求,B也同样收到多少请求,很有可能把B搞挂。
额外小知识科普:
1)服务器为什么在请求激增的情况下可能会挂?
一台服务器就是一台电脑,上面提供着一些硬件资源(cpu,内存,宽带......)
再好的电脑,硬件资源是有限的,服务器每收到一个请求,处理请求时都要执行一些代码,需消耗一定的硬件资源。
当这些请求消耗的总硬件资源超过了机器提供的上限,那机器就会卡死,程序崩溃,服务器就挂了
2)在请求激增的时候,为啥A服务器不会挂而B服务器更容易挂
A服务器是一个“网关服务器”,工作是收到请求,并转发给其他服务器,消耗的硬件资源更少。
在同样配置下,消耗的硬件资源更少,就能处理更多请求,所以不会那么容易挂。
B服务器是真正干活的服务器,需要真正执行一系列逻辑,消耗的资源更多,在请求激增的情况下更容易挂。
类似的,Mysql这样的数据库,处理每个请求时,做个工作是比较多的,消耗的硬件资源也多,因此Mysql也是系统中容易挂的部分。
3)生产者消费者模式的代价
1.需要更多的机器来部署这些队列。(代价小,小机器不怎么值钱)
2.A和B服务器之间通信的时延会更长。
如果A和B之间的调用要求的响应时间短(银行取钱),就不太适合了。
三.BlockingQueue
阻塞队列在Java标准库中提供现成封装——BlockingQueue。
1.实现
BlockingQueue<String> queue=new ArrayBlockingQueue<>(100);//这个参数是阻塞队列的容量
BlockingQueue是一个接口,不能直接去new,应该去new实现这个接口的类。
2.使用
Queue提供的一些操作,在BlockingQueue中同样能使用(pull,offer,但用的少)
BlockingQueue提供了两个专属方法
1)put入队列
queue.put();
public class Dem1 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue=new ArrayBlockingQueue<>(3);//这个参数是阻塞队列的容量
queue.put("111");
System.out.println("put成功");
queue.put("111");
System.out.println("put成功");
queue.put("111");
System.out.println("put成功");
queue.put("111");
System.out.println("put成功");
}
}
代码中4次put,却只有3次成功,这是因为阻塞队列设置的容量为3,第四个put阻塞了。
在jconsole上,也能查看到当前线程处于WAITING状态
2)take出队列
queue.take();
public class Dem1 {
public static void main(String[] args) throws InterruptedException {
BlockingQueue<String> queue=new ArrayBlockingQueue<>(3);//这个参数是阻塞队列的容量
queue.put("111");
System.out.println("put成功");
queue.put("111");
System.out.println("put成功");
queue.take();
System.out.println("take成功");
queue.take();
System.out.println("take成功");
queue.take();
System.out.println("take成功");
}
}
当take()次数多于阻塞队列中元素的值时,也会发生阻塞。
3.生产者消费者模型代码
public class Demo2 {
public static void main(String[] args) {
BlockingQueue<Integer> queue=new ArrayBlockingQueue<>(1000); //设置容量为1000
Thread t1=new Thread(()->{
int i=0;
//生产者线程
while(true){
try {
queue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("成功生产元素"+i);
i++;
}
});
Thread t2=new Thread(()->{
//消费者线程
while (true){
try {
int i= queue.take();
System.out.println("消费元素"+i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
}
}
若想要生产慢一些,可通过sleep控制
Thread t1=new Thread(()->{
int i=0;
//生产者线程
while(true){
try {
queue.put(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("成功生产元素"+i);
i++;
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
可以发现,生产者线程慢时,生产一个,消费一个。
上述程序中,一个线程生产,一个线程消费。
在实际开发中,通常多个线程生产,多个线程消费。
四.自己实现一个阻塞队列
1.思路
利用数组实现阻塞队列
head表示头,tail表示尾。当数据put()进队列时,tail向后面移。
若是take()出队列,则将head往后移。
当tail移动到数组末尾时,若队列未满,则将tail移回初始的位置。
若队列已满,则可以浪费最后一个位置,tail指向最后的位置。
2.代码实现
首先先不考虑阻塞与线程安全,代码如下:
class MyBlockingQueue{
private String[] data=null;
public MyBlockingQueue(int capacity){
data=new String[capacity];
}
private int head=0;
private int tail=0;
private int size=0;
public void put(String s){
if(size==data.length){
return ;
}
data[tail]=s;
tail++;
if(tail>data.length){
tail=0;;
}
size++;
}
public String take(){
String ret="";
if(size==0){
return null;
}
ret=data[head];
head++;
if(head>data.length){
head=0;
}
size--;
return ret;
}
}
考虑到线程安全与阻塞,代码可进一步优化。
在多线程中,修改操作若为非原子有很大可能造成线程安全问题
因此,我们可以为put()和take()上锁打包成原子操作的。
其次,当队列未满或为空时,可以通过wait()和notify()进行阻塞和通知。
代码如下
class MyBlockingQueue{
private String[] data=null;
public MyBlockingQueue(int capacity){
data=new String[capacity];
}
private int head=0;
private int tail=0;
private int size=0;
public void put(String s) throws InterruptedException {
synchronized (this){
if(size==data.length){
// return ;
this.wait();
}
data[tail]=s;
tail++;
if(tail>=data.length){
tail=0;;
}
size++;
this.notify();
}
}
public String take() throws InterruptedException {
String ret="";
synchronized (this){
if(size==0){
// return null;
this.wait();
}
ret=data[head];
head++;
if(head>data.length){
head=0;
}
size--;
}
this.notify();
return ret;
}
}
以上便是所有内容,如有不对,欢迎指正。