一、什么是阻塞队列?
首先,队列我们应该都了解,是一种常见的数据结构,特点是先进先出(FIFO)
结合生活中的例子,当火锅店中人满为患座位占满时,通常的做法是安排后来的顾客在候客区等待正在就餐的顾客,而不是让顾客直接离开。此时的场景对于商家来说是希望存在阻塞(候客区)的。而在实际开发中,有时会遇到需要阻塞或者不得不阻塞的场景,为了对阻塞的线程进行管理,调度,就有了本节所讲的阻塞队列接口java.util.concurrent.BlockingQueue。这里对其下一个简单的定义:
阻塞:必须要阻塞/不得不阻塞
阻塞队列是一个队列,在数据结构中起到的作用如下图:
阻塞队列的特点:
- 当队列是空的,从队列中获取元素的操作将会被阻塞
- 当队列是满的,从队列中添加元素的操作将会被阻塞
- 试图从空的队列中获取元素的线程将会被阻塞,直到其他线程往空的队列插入新的元素
- 试图向已满的队列中添加新元素的线程将会被阻塞,直到其他线程从队列中移除一个或多个元素或者完全清空,使队列变的空闲起来并后续新增
二、为什么要用阻塞队列
JUC发布之前,在多线程环境下,我们不仅要控制线程的阻塞和唤醒,还要兼顾效率和线程安全,给我们的程序带来了更大的复杂度。
在多线程领域,在某些情况下会挂起(wait)线程(即阻塞),一旦条件满足,被挂起的线程又会自动被唤醒(notify)。在以往的多线程业务开发中,往往需要我们自己来编写代码来判断什么时候调用生产线程,什么时候调用消费线程。而BlockingQueue帮我们在“生产-消费”关系中充当了条件判断的角色,因此我们就不需要关心什么时候阻塞线程,什么时候唤醒线程,BlockingQueue帮我们一手包办啦。
三、如何使用阻塞队列
首先,让我们来看看BlockingQueue的体系架构:
没错,你没看错,神秘的BlockingQueue原来是Queue的子接口!而Queue又是Collection接口的子接口,与我们常用的List和Set平级。BlockingQueue的实现类有很多,下面对这些类进行简单的介绍:
- ArrayBlockingQueue:由数组结构组成的有界阻塞队列
- LinkedBlockingQueue:由链表结构组成的有界(但大小默认值为Integer.MAX_VALUE)阻塞队列
- PriorityBlockingQueue:支持优先级排序的无界阻塞队列
- DelayQueue:使用优先级队列实现的延迟无界阻塞队列
- SynchronousQueue:不存储元素的阻塞队列,也即单个元素的队列(队列中永远只有一个元素,生产一个就消费一个)
- LinkedTransferQueue:由链表组成的无界阻塞队列
- LinkedBlockingDeque:由链表组成的双向阻塞队列
以上常用的阻塞队列有两个,即ArrayBlockingQueue和LinkedBlockingQueue,本文针对ArrayBlockingQueue进行单独介绍。
了解了BlockingQueue的体系架构之后,让我们来看看BlockingQueue有哪些常用的方法。BlockingQueue的常用方法总结起来有四大类型:抛出异常、特殊值、阻塞和超时退出,这些方法的归类如下所示:
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
移除 | remove() | poll() | take() | poll(time,unit) |
检查 | element() | peek() | 不可用 | 不可用 |
下面通过代码演示更好的理解上面的归类
1.抛出异常类型
1)插入add(e)
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第一组抛出异常类型的插入add(e)方法
testAdd1();
}
private static void testAdd1() {
//初始化容量为3的阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//add(e)返回boolean类型:BlockingQueue public abstract boolean add(String e)
System.out.println(blockingQueue.add("A"));
//由于ArrayBlockingQueue底层数据结构是数组,类似于ArrayList,所以允许该队列存放相同元素
System.out.println(blockingQueue.add("A"));
System.out.println(blockingQueue.add("B"));
//此时放入元素的数量超出该队列的容量
//System.out.println(blockingQueue.add("C"));
}
}
运行结果:
结论:当使用add(e)插入队列的元素数量没有超过队列的容量时,程序正常执行
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第一组抛出异常类型的插入add(e)方法
testAdd1();
}
private static void testAdd1() {
//初始化容量为3的阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.add("A");
//由于ArrayBlockingQueue底层数据结构是数组,类似于ArrayList,所以允许该队列存放相同元素
blockingQueue.add("A");
blockingQueue.add("B");
//此时放入元素的数量超出该队列的容量,报错
blockingQueue.add("C");
}
}
运行结果:
结论:当使用add()插入队列的元素数量超出该队列的容量时,报队列满的非法状态异常Queue full
2)移除remove()
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第一组抛出异常类型的移除remove()方法
testRemove1();
}
private static void testRemove1() {
//初始化容量为3的阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.add("A");
blockingQueue.add("B");
blockingQueue.add("C");
//根据队列的数据结构特点,移除顺序为A-B-C
System.out.println(blockingQueue.remove());
//Queue public abstract String remove()
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
//System.out.println(blockingQueue.remove());
}
}
运行结果:
结论:当阻塞队列有元素时,使用remove()移除元素时,程序正常运行
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第一组抛出异常类型的移除remove()方法
testRemove1();
}
private static void testRemove1() {
//初始化容量为3的阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.add("A");
blockingQueue.add("B");
blockingQueue.add("C");
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
System.out.println(blockingQueue.remove());
}
}
运行结果:
结论:当阻塞队列为空时,使用remove()移除元素时,程序报NoSuchElementException异常
3)检查element()
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第一组抛出异常类型的检查元素element()方法
testElement1();
}
private static void testElement1() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//blockingQueue.add("A");
//blockingQueue.add("B");
//检查阻塞队列的队首是哪个元素Queue public abstract String element()
System.out.println(blockingQueue.element());
}
}
运行结果:
结论:同remove()方法,当阻塞队列中没有元素时,调用element()方法会报NoSuchElementException异常
2.特殊值类型
1)插入offer(e)
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第二组特殊值类型的插入offer(e)方法
testOffer2();
}
private static void testOffer2() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
//BlockingQueue public abstract boolean offer(String e)
System.out.println(blockingQueue.offer("A"));
System.out.println(blockingQueue.offer("A"));
System.out.println(blockingQueue.offer("B"));
//此时放入元素的数量超出该队列的容量,返回false
System.out.println(blockingQueue.offer("C"));
}
}
运行结果:
结论:与抛出异常的add(e)方法不同,当向阻塞队列中新增元素的数量超过队列容量时,offer()方法返回false,不会报异常
2)移除poll()
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第二组特殊值类型的移除poll()方法
testPoll2();
}
private static void testPoll2() {
//初始化容量为3的阻塞队列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
blockingQueue.add("A");
blockingQueue.add("B");
//根据队列的数据结构特点,移除顺序为A-B-C
//Queue public abstract String poll()
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
System.out.println(blockingQueue.poll());
}
}
运行结果:
结论:与抛出异常的remove()方法不同,当阻塞队列中不存在可移除的元素时,调用poll()方法返回null,不会报异常
3)检查peek()
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args) {
//测试第二组特殊值类型的检查元素peek()方法
testPeek2();
}
private static void testPeek2() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(3);
/*blockingQueue.add("A");
blockingQueue.add("B");*/
//检查阻塞队列的队首是哪个元素Queue public abstract String peek()
System.out.println(blockingQueue.peek());
}
运行结果:
结论:与抛出异常的element()方法不同,当阻塞队列中不存在元素时,调用peek()方法返回null,不会报异常
3.阻塞类型
1)插入put(e)
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class BlockingQueueDemo {
public static void main(String[] args){
//测试第三组阻塞类型的插入元素方法put(e)
testPut31();
//模拟多线程环境下解除put(e)方法造成的线程阻塞
//testPut32();
}
private static void testPut31() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
System.out.println(blockingQueue.add("A"));
try{
//BlockingQueue public abstract void put(String e) throws InterruptedException
//插入新的元素“C”,由于队列的容量为1,所以该队列陷入阻塞状态
blockingQueue.put("C");
System.out.println("程序已经被阻塞,随便敲点什么都不会被执行。。。");
}catch (InterruptedException e){
e.printStackTrace();
}
}
private static void testPut32(){
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
System.out.println(Thread.currentThread().getName()+"-----"+blockingQueue.add("A"));
System.out.println(Thread.currentThread().getName()+"-----初始队首元素:"+blockingQueue.element());
//新建一个子线程
new Thread(()->{
//睡眠3秒
try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); }
//移除队列中的“A”元素
System.out.println(Thread.currentThread().getName()+"-----移除队列中的元素:"+blockingQueue.remove());
}).start();
try{
System.out.println(Thread.currentThread().getName()+"-----继续正常执行");
//插入新的元素“C”,此时由于队列中的“A”还没有被移除,队列的容量为1,所以该队列陷入阻塞状态
blockingQueue.put("C");
System.out.println(Thread.currentThread().getName()+"-----插入新元素后队首元素:"+blockingQueue.element());
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
testPut31运行结果:
testPut32运行结果:
结论:当要插入元素的阻塞队列已满时,使用put(e)方法会造成程序一直处于阻塞状态,直到从队列中移除元素才会将新元素插入该队列。
2)移除take()
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args){
//测试第三组阻塞类型的移除元素take()方法
//testTake31();
//模拟多线程环境下解除take()方法造成的线程阻塞
//testTake32();
}
private static void testTake31() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
System.out.println(blockingQueue.add("A"));
try{
//BlockingQueue public abstract String take() throws InterruptedException
//移除队首的元素,返回被移除的元素
System.out.println(blockingQueue.take());
//由于此时阻塞队列中已经没有元素,所以队列进入阻塞状态
System.out.println(blockingQueue.take());
System.out.println("程序已经被阻塞,随便敲点什么都不会被执行。。。");
}catch (InterruptedException e){
e.printStackTrace();
}
}
private static void testTake32() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
System.out.println(Thread.currentThread().getName()+"-----初始化插入元素:"+blockingQueue.add("A"));
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"-----"+blockingQueue.remove());
System.out.println(Thread.currentThread().getName()+"-----元素A已经被移除,队列中没有元素了。。。");
//线程睡眠3秒
try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"-----新插入元素结果:"+blockingQueue.add("D"));
},"孙培阳").start();
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"-----进入李扬扬线程。。。");
try {
System.out.println(Thread.currentThread().getName()+"-----"+blockingQueue.take());
System.out.println(Thread.currentThread().getName()+"-----不等到队列中有元素,我就不执行。。。");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"李扬扬").start();
}
}
testTake31运行结果:
testTake32运行结果:
结论:当要移除元素的阻塞队列为空时,使用take()方法会造成程序一直处于阻塞状态,直到向队列中插入新的元素才会将该新元素从队列中移除。
4.超时退出类型
1)插入方法offer(e,time,unit)
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args){
//测试第四组超时退出类型offer(e,time,unit)方法
testOffer41();
//模拟多线程环境下使用offer(e,time,unit)插入新元素
//testOffer42();
}
private static void testOffer41() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
System.out.println(Thread.currentThread().getName()+"-----初始化插入元素:"+blockingQueue.add("A"));
try {
//BlockingQueue public abstract boolean offer(String e, long timeout, TimeUnit unit) throws InterruptedException
System.out.println("超时退出插入方法结果:"+blockingQueue.offer("B", 3, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void testOffer42() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
System.out.println(Thread.currentThread().getName()+"-----初始化插入元素:"+blockingQueue.add("A"));
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"-----超时退出插入方法结果:"+blockingQueue.offer("B", 3, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
},"孙培阳").start();
new Thread(()->{
try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"-----移除元素结果:"+blockingQueue.remove());
System.out.println(Thread.currentThread().getName()+"-----获取队列中最新元素:"+blockingQueue.element());
},"李扬扬").start();
}
}
testOffer41运行结果:
testOffer42运行结果:
结论:当要插入元素的阻塞队列已满时,使用offer(e,time,unit)方法会造成程序在指定时间内处于阻塞状态,如果在指定时间内该队列一直是满的,则结束程序。
2)移除元素poll(time,unit)
package cn.edu.zknu.juc;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
public class BlockingQueueDemo {
public static void main(String[] args){
//testPoll41();
testPoll42();
}
private static void testPoll41() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
System.out.println("插入新元素:"+blockingQueue.add("A"));
System.out.println("移除元素:"+blockingQueue.remove());
try {
//BlockingQueue public abstract String poll(long timeout, TimeUnit unit) throws InterruptedException
System.out.println("超时退出移除的元素:"+blockingQueue.poll(3, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void testPoll42() {
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1);
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"-----开始移除元素");
System.out.println(Thread.currentThread().getName()+"-----超时退出移除方法结果:"+blockingQueue.poll(3, TimeUnit.SECONDS));
} catch (InterruptedException e) {
e.printStackTrace();
}
},"孙培阳").start();
new Thread(()->{
try { TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
System.out.println(Thread.currentThread().getName()+"-----插入元素结果:"+blockingQueue.add("P"));
},"李扬扬").start();
}
testPoll41运行结果:
testPoll42运行结果:
结论:当使用poll(time,unit)方法对阻塞队列进行移除元素时,如果该队列的状态为空队列时,程序会在指定时间内等到队列中有元素时再进行移除,如果超过指定时间队列中仍没有元素可以移除,则结束程序。
通过以上对ArrayBlockingQueue不同方法的探究,我们不难总结出以下结论:
到这里,我们对ArrayBlockingQueue有了基本的认识和了解,如果感兴趣,可以看看这些方法底层的源码,了解其通过ReentrantLock对各个方法进行线程安全以及线程通信的工作原理,对于以后的工作和学习都是有帮助的,暂时先到这里。