概念
阻塞队列是带有阻塞功能的队列
特性
当队列满的时候,继续入队列,就会出现阻塞,阻塞到其他线程从队列中取走元素为止
当队列空的时候,继续出队列,也会发生阻塞,阻塞到其他线程往队列中添加元素为止
特点
阻塞队列有着解耦合和削峰填谷的主要特点
1.解耦合
两个服务器A,B,服务器A负责输送用户请求给服务器B,服务器B负责解决用户请求,但是两个服务器之间不是直接联系起来的,而是有一个阻塞队列在两个服务器之间,服务器A将请求输送给阻塞队列,服务器B从阻塞队列中获取请求,这样两个服务器之间就不是直接联系,耦合性就被降低。
耦合性降低可以避免出现一个服务器发生错误导致另一个服务器也发生错误的情况
2.削峰填谷
同样是两个服务器A,B,服务器A负责输送用户请求给服务器B,有一个阻塞队列在两个服务器之间,当出现莫种特殊情况,导致服务器A输送大量的数据到队列中,由于阻塞队列满了再添加数据就会发生阻塞等待,所以服务器B处理数据的节奏不会被影响,但要是没有阻塞队列,服务器A和服务器B是直接连接的话,很可能服务器A输送大量的请求给服务器B会导致服务器B崩溃
系统提供的阻塞队列的使用
BlockingQueue<Integer> queue=new ArrayBlockingQueue(); //底层是顺序表
BlockingQueue<Integer> queue=new LinkedBlockingQueue(); //底层是链表
BlockingQueue<Integer> queue=new PriorityBlockingQueue(); //底层是优先级队列(堆)
调用put方法是添加数据到队列中,调用take方法是弹出队列中的数据,由于BlockingQueue实现了Queue接口,所以它也有offer和poll等方法,但只有put和take方法是带阻塞的,所以推荐使用put和take方法
代码展示
class MyBlockingQueue{
//用循环顺序表来作为阻塞队列的底层实现
private String[]items=new String[1000];
//数据存在的范围是[head,tail)
//避免出现内存可见性和指令重排序的问题,要对变量加上volatile
//指向头部的指针
private volatile int head=0;
//指向尾部的指针
private volatile int tail=0;
//记录阻塞队列中的数据个数
private volatile int size=0;
//put和take方法在多线程中涉及到多个线程改变同一个变量,所以要加上锁
//向阻塞队列插入数据
public void put(String elem) throws InterruptedException {
synchronized(this){
//判断队列是否满了
while (size>=items.length){ //不一定阻塞等待是被notify正常唤醒的,也就不一定队列真的不满了,所以要用while循环多次判断
//直到真的队列没满才进行接下来的操作(wait进行阻塞等待推荐和while一起使用,多次判断是否满足唤醒条件)
this.wait(); //当队列满了就进入阻塞等待
}
items[tail]=elem;
tail++;
size++;
if(tail>=items.length){
tail=0;
}
this.notify(); //当向阻塞队列插入数据后,便可以唤醒弹出数据的阻塞状态
}
}
//弹出阻塞队列中的数据
public String take() throws InterruptedException {
synchronized(this){
//判断阻塞队列是否为空
while (size==0){//不一定阻塞等待是被notify正常唤醒的,也就不一定队列真的不为空,所以要用while循环多次判断
//直到真的队列不为空才进行接下来的操作(wait进行阻塞等待推荐和while一起使用,多次判断是否满足唤醒条件)
this.wait();
}
String elem=items[head];
head++;
size--;
if(head>=items.length){
head=0;
}
this.notify(); //弹出一个数据后队列不满了就可以唤醒插入数据的阻塞状态
return elem;
}
}
}
代码分析
1.该代码中实现阻塞队列的底层结构是很简单的循环数组
2.由于要满足多线程编程,而put和take操作涉及到多个线程修改同一个变量的问题,所以需要用synchronized给put和take方法加锁,而为了防止出现内存可见性和指令重排序问题,对于在多线程中要进行读取修改的变量加上volatile关键字,防止编译器进行优化。
关于synchronized和内存可见性和指令重排序可以看线程安全问题,线程安全问题(内存可见性),线程安全问题(指令重排序)
3.如何实现阻塞呢?在put方法中,如果队列满了,那就要进入阻塞等待,就用synchronized加锁的对象调用wait方法进入阻塞等待,只有当take方法执行成功,才能调用notify唤醒put方法中阻塞等待的线程。
同样在take方法中,如果队列空了,那就要进入阻塞等待,就用synchronized加锁的对象调用wait方法进入阻塞等待,只有当put方法执行成功,才能调用notify唤醒take方法中阻塞等待的线程。关于wait于notify可以看通过wait和notify来协调线程执行顺序
4.put和take方法要对同一个对象进行加锁,因为put和take中要修改的变量有相同的,所以当多个线程调用put和take方法时存在线程安全问题,所以要对同一个对象进行加锁。
5.判断是否进入阻塞等待的条件是一个while循环(推荐wait()与while一起使用),因为不一定阻塞等待wait是被notify正常唤醒的,也就不一定是否真的满足队列不满或者不空的条件,所以要进行多次判断,要是队列依然还是满或空的话就要再次进入阻塞等待