什么是阻塞队列
阻塞队列是一种特殊的队列,也遵循“先进先出”
阻塞队列能是⼀种线程安全的数据结构,并且具有以下特性:
当队列满时,入队列会阻塞,直到有其他线程从队列中取出元素。
当队列空时,出队列会阻塞,直到有其他线程从往队列中插入元素。
相比于普通队列,阻塞队列是线程安全的.
阻塞队列的⼀个典型应⽤场景就是"⽣产者消费者模型".这是⼀种⾮常典型的开发模型.
生产者消费者模型
生产者消费者模型就是通过一个容器来解决生产者和消费者之间的强耦合问题
⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取.
阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒.(削峰填⾕)
阻塞队列也能使⽣产者和消费者之间解耦.
那么阻塞队列这么优秀,引入的代价是什么呢?
最明显的一点,由于需要部署阻塞队列,所以需要加钱,加机器。
由于新增了服务器,所以需要维护的服务器也变多了。
效率会下降,从生产者传给阻塞队列再传给消费者,这样的能耗变多了,速度也不如直接传输。
Java标准库中的阻塞队列
在Java标准库中,阻塞队列为BlockingQueue,这时一个接口,真正实现阻塞队列的内容的是,LinkedBlockingQueue,ArrayBlockingQueue,PriorityBlockingQueue三个类。
阻塞队列相较于普通队列有两个特殊的方法,put(入列)和take(出列),put和take在用法上比offer和poll多了阻塞这一特性.
在对BlockingQueue实例化时,使用的构造方法需要传入一个参数,来确定阻塞队列的容量:
BlockingQueue<String> queue1 = new LinkedBlockingQueue<>(100);;
BlockingQueue<String> queue2 = new ArrayBlockingQueue<>(100);
BlockingQueue<String> queue3 = new PriorityBlockingQueue<>(100);
⽣产者消费者模型的简单应用:
public static void main(String[] args) throws InterruptedException {
BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>();
Thread customer = new Thread(() -> {
while (true) {
try {
int value = blockingQueue.take();
System.out.println("消费元素: " + value);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "消费者");
customer.start();
Thread producer = new Thread(() -> {
Random random = new Random();
while (true) {
try {
int num = random.nextInt(1000);
System.out.println("⽣产元素: " + num);
blockingQueue.put(num);
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "⽣产者");
producer.start();
customer.join();
producer.join();
}
阻塞队列实现
要实现阻塞队列,只需要在普通队列的基础上,实现阻塞和线程安全即可。
普通队列:
public class MyBlockingQueue {
public int[] elems = null;
public int head = 0;//队首表示
public int tail = 0;//队尾表示
public int usedSize = 0;//元素个数
public MyBlockingQueue(int size) {
this.elems = new int[size];
}
public void put(int n){
if(usedSize == elems.length){//队列满
//执行阻塞
return;
}
//此时可以正常入队
elems[usedSize] = n;
usedSize++;
tail++;
if(tail >= elems.length){//如果队尾表示超出队列,则重置
tail = 0;
}
}
public int take(){
int elem;
if(usedSize == 0){//队列空
//执行阻塞
return Integer.MIN_VALUE;
}
//此时可以正常出队
elem = elems[head];
head++;
usedSize--;
if(head >= elems.length){
head = 0;
}
return elem;
}
}
上面这个普通队列的代码在多线程中是不安全的
例如,t1和t2两个线程同时进行put而此时队列仅有一个剩余空间,当t1判断完队列是否满,想要执行入队操作时,t1线程被调度走,转为t2执行,如果这时t2完整的执行了put操作,那么此时队列为满。但是回到t1后,由于已经判断过队列不满成立,所以t1会继续入队,导致有数据被覆盖且usedSize数值不正确。
想要解决这个由“读写”产生的问题,只需要进行加锁就可以了。
public class MyBlockingQueue {
public int[] elems = null;
public int head = 0;//队首表示
public int tail = 0;//队尾表示
public int usedSize = 0;//元素个数
Object lock = new Object();
public MyBlockingQueue(int size) {
this.elems = new int[size];
}
public void put(int n){
synchronized (lock){//对读写操作上锁
if (usedSize == elems.length) {//队列满
//执行阻塞
return;
}
//此时可以正常入队
elems[usedSize] = n;
usedSize++;
tail++;
if (tail >= elems.length) {//如果队尾表示超出队列,则重置
tail = 0;
}
}
}
public int take(){
int elem;
synchronized (lock){//对读写操作上锁
if (usedSize == 0) {//队列空
//执行阻塞
return Integer.MIN_VALUE;
}
//此时可以正常出队
elem = elems[head];
head++;
usedSize--;
if (head >= elems.length) {
head = 0;
}
}
return elem;
}
}
保证了线程安全后,来解决另一个问题:阻塞等待。
想要线程主动阻塞等待,也很简单:使用wait
只要在判断到满或空后进行wait,在另一个线程对对零进行出列或者入列操作后,对其他线程进行唤醒,就可以实现阻塞队列的这一特性:
public class MyBlockingQueue {
public int[] elems = null;
public int head = 0;//队首表示
public int tail = 0;//队尾表示
public int usedSize = 0;//元素个数
Object lock = new Object();
public MyBlockingQueue(int size) {
this.elems = new int[size];
}
public void put(int n) throws InterruptedException {
synchronized (lock){//对读写操作上锁
if (usedSize == elems.length) {//队列满
lock.wait();//阻塞等待
}
//此时可以正常入队
elems[usedSize] = n;
usedSize++;
tail++;
if (tail >= elems.length) {//如果队尾表示超出队列,则重置
tail = 0;
}
lock.notify();//唤醒由于队列空而阻塞的线程
}
}
public int take() throws InterruptedException {
int elem;
synchronized (lock){//对读写操作上锁
if (usedSize == 0) {//队列空
lock.wait();//阻塞等待
}
//此时可以正常出队
elem = elems[head];
head++;
usedSize--;
if (head >= elems.length) {
head = 0;
}
lock.notify();//唤醒由于队列满而阻塞的线程
}
return elem;
}
}
这样一来,我们就实现了线程安全和阻塞等待两个步骤。
但是,真的对吗?
试想这样的场景:在队列满时t1线程执行了put,那么t1就会由于队列满而进入wait,此时t2也进行了put,那么t2也会进入wait,之后t3线程执行了take,在take的最后,会随机唤醒t1或t2,假设唤醒了t1,那么在t1的put的最后,也会执行唤醒操作,此时唯一在wait的线程只有t2,那么t2就会被错误的唤醒,如果t2继续执行,就会出现有数据被覆盖且usedSize不正确的现象。
如何解决?
这个问题的产生,是t1唤醒了同样在等待出队操作唤醒的t2,由于此时t2没有判断这个唤醒者是不是出队操作,所以才导致t2错误的开始执行。那只需要让t2判断一下现在队列是否满就行,如何判断?再套用一个if?显然不可行,如果这样的情况再发生一次,那么这个套用的if也没有任何作用。想要解决 就需要让t2被唤醒再执行一次判断,并且保证之后的每次唤醒都要进行判断。
那这么一想,解决这个问题就变得简单了:使用while循环替换if,既然需要不断的进行判断,且判断成功后线程会阻塞等待,那么就可以使用while,让线程阻塞被唤醒后再进行循环判断,如果依旧满足循环条件,那线程再进入阻塞等待就好。不满足,那就可以正确执行.
public class MyBlockingQueue {
public int[] elems = null;
public int head = 0;//队首表示
public int tail = 0;//队尾表示
public int usedSize = 0;//元素个数
Object lock = new Object();
public MyBlockingQueue(int size) {
this.elems = new int[size];
}
public void put(int n) throws InterruptedException {
synchronized (lock){//对读写操作上锁
while (usedSize == elems.length) {//队列满
lock.wait();//阻塞等待
}
//此时可以正常入队
elems[usedSize] = n;
usedSize++;
tail++;
if (tail >= elems.length) {//如果队尾表示超出队列,则重置
tail = 0;
}
lock.notify();//唤醒由于队列空而阻塞的线程
}
}
public int take() throws InterruptedException {
int elem;
synchronized (lock){//对读写操作上锁
while (usedSize == 0) {//队列空
lock.wait();//阻塞等待
}
//此时可以正常出队
elem = elems[head];
head++;
usedSize--;
if (head >= elems.length) {
head = 0;
}
lock.notify();//唤醒由于队列满而阻塞的线程
}
return elem;
}
}