首先来看扩展关系图:
BlockingQueue是一个接口,其底下的实现类有很多,其中ArrayBlockingQueue和LinkedBlockingQueue是常用的,另外SynchronousQueue同步队列也是重点,故此本文只对这3个队列进行概述。
▶ BlockingQueue 是什么?
一个阻塞队列,是Java util.concurrent包下重要的数据结构,提供了线程安全的队列访问方式。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。
▶ BlockingQueue的实现类有:
参数 | 描述 |
---|---|
ArrayBlockingQueue | 数组结构的有界阻塞队列。创建必须指定大小 |
LinkedBlockingQueue | 链表结构的有界阻塞队列。创建未指定大小,默认为Integer.MAX_VALUE-无界 |
SynchronousQueue | 无界,队列大小为0,而最大创建线程数:Integer.MAX_VALUE , 线程不足,会一直创建新线程(交给线程处理,不存入队列中) |
PriorityBlockingQueue | 一个支持优先级排序的无界阻塞队列。 |
DelayQueue | 一个使用优先级队列实现的无界阻塞队列,即在延迟期满时才能从中提取元素 |
LinkedTransferQueue | 链表结构的无界阻塞队列。与SynchronousQueue类似,还含有非阻塞方法。 |
LinkedBlockingDeque | 链表结构的无界双向阻塞队列。基于链表的先进先出队列,如创建时未指定队列大小,则默认为Integer.MAX_VALUE; |
扩展一个知识点: Integer.MAX_VALUE (约21亿),假设你的队列大小为此值,在并发情况下,如果存在大量请求打过来,你的处理速度远跟不上任务的请求数量,就会导致队列的请求任务堆积,在达到21亿的请求任务数前,你的内存可能早就爆炸了,从而造成OOM。
▶ BlockingQueue的操作方法:
阻塞队列提供了4种API
方法 | 抛出异常 | 不抛异常,有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
添加 | add | offer | put | offer (Timeout) |
移除 | remove | poll | take | poll (Timeout) |
检测队首元素 | element | peek | / | / |
下面将用代码来说明上述阻塞队列的 4种处理方式
方式一:抛出异常
/**
抛出异常
add:超出队列指定容量,会报:Queue full 异常
remove:(每调用一次,移除一个元素),队列为空继续remove会报:NoSuchElementException
*/
public static void main(String[] args){
// 1. 定义一个队列容量为2,基于数组的阻塞队列
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
// 2. 添加元素 true:添加成功 false:添加失败
System.out.println(queue.add("1"));
System.out.println(queue.add("2"));
//queue.add("3"); // 使用add超出队列指定容量,会报:Queue full异常
// 3. 获取队列首部元素 :1
System.out.println(queue.element());
// 4.移除元素
//队列为空继续用remove:NoSuchElementException
System.out.println("移除元素:"+queue.remove());
System.out.println("队列:"+queue);
}
执行结果:
我们把 // queue.add("3"); 的注释打开:添加第3个元素时,报Queue full异常
方式二:不抛异常,有返回值
/**
有返回值,没有异常
offer:返回false,超出容量后不再添加入队
poll:(每调用一次,移除一个元素)队列为空,继续poll会输出null
*/
public static void main(String[] args){
// 1.定义队列容量为2
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
// 2.添加元素
System.out.println(queue.offer("3"));
System.out.println(queue.offer("4"));
System.out.println(queue.offer("5")); // false 超出容量后不再添加入队
// 3.获取队列首部元素 :3
System.out.println(queue.peek());
// 4.移除元素
System.out.println("移除元素:"+queue.poll());
System.out.println("移除元素:"+queue.poll());
// 注意:队列最多加入2个,上述已经全部移除了,此时继续poll会输出null
System.out.println("移除元素: "+queue.poll());
System.out.println("队列:"+queue);
}
执行结果:1. 队列已满,继续offer添加元素返回false 2. 针对空队列继续poll移除,会返回null
方式三:阻塞等待
/**
等待,阻塞(一直等待)
put/take:超出长度无法添加,等待阻塞 / 队列内没有元素,等待阻塞
———— 直到添加成功或删除成功,程序才会结束
*/
public static void main(String[] args) throws InterruptedException {
// 1.定义容量为2的队列
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
// 2.添加元素
queue.put("a");
queue.put("b");
//queue.put("c"); // 等待阻塞,直到添加成功,程序才会结束
// 3.移除元素
System.out.println("移除元素:"+queue.take());
System.out.println("移除元素:"+queue.take());
// 注意:此时队列已为空,继续take则进入等待阻塞,直到队内有元素可删除,程序才结束
System.out.println("移除元素:"+queue.take());
System.out.println("队列:"+queue);
}
执行结果:
方式四:超时等待
/**
等待,阻塞(超时等待)
offer/poll:超出指定时间,还未操作成功,则忽略此次添加/删除
*/
public static void main(String[] args) throws InterruptedException {
// 1.定义容量为2的队列
ArrayBlockingQueue queue = new ArrayBlockingQueue<>(2);
// 2.添加元素
queue.offer("A");
queue.offer("B");
// 3.队列已满,使用超时添加:超时2秒后,还未添加成功,则忽略此次offer
queue.offer("C",2, TimeUnit.SECONDS);
// 4.移除元素
System.out.println("移除元素:"+queue.poll());
System.out.println("移除元素:"+queue.poll());
// 同超时offer
System.out.println("移除元素: "+queue.poll(2, TimeUnit.SECONDS));
System.out.println("队列:"+queue);
}
执行结果:添加第三个元素,等待了2秒后添加失败,则忽略此次添加继续执行后面代码
▶ 队列类型
SynchronousQueue 同步队列
特殊的阻塞队列,容量为0,也无法设置容量大小。put了一个元素,必须等待消费者线程take取出来,才能再进行下一个元素的插入操作
类似一个中介,从生产者手中拿到任务直接转交给消费者,直接传递机制,而非存储元素
代码示例:
public static void main(String[] args) {
// 1.初始化一个同步队列
BlockingQueue<String> queue = new SynchronousQueue<String>();
// 2.定义2个线程分别为线程A、线程B,线程A添加元素,线程B取出元素
new Thread(()->{
try {
// 添加3个元素
System.out.println(Thread.currentThread().getName()+":put 1");
queue.put("1");
System.out.println(Thread.currentThread().getName()+":put 2");
queue.put("2");
System.out.println(Thread.currentThread().getName()+":put 3");
queue.put("3");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程A").start();
new Thread(()->{
try {
// 每隔3秒取出一个元素
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"=>"+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"=>"+queue.take());
TimeUnit.SECONDS.sleep(3);
System.out.println(Thread.currentThread().getName()+"=>"+queue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"线程B").start();
}
执行结果:put一个元素,须等待消费者线程取出来,下一个元素才能put进去
LinkedBlockingQueue 链表队列
基于链表实现,创建时如不指定大小,默认为Integet.MAX_VALUE,如果生产者的速度远大于消费者速度,则队列内会堆积请求,可能会引起OOM,由于是基于链表模式,每次放入元素会构造一个新节点对象,大量并发下同样会对GC造成一定影响。阻塞方式使用通知模式来实现,生产者和消费者分别使用两把重入锁实现同步,提高系统并发度
ArrayBlockingQueue 数组队列
基于数组实现的阻塞队列,创建对象时必须指定容量大小,且可指定是否公平(默认非公平,即不保证等待时间最长的队列最优先能够访问队列),存取元素的阻塞方法put/take,使用通知模式来实现。生产者和消费者存取数据用的同一把重入锁,无法真正实现生产者和消费者的并行
put/take方法实现的阻塞方式——通知模式(await等待 / signal唤醒),源码如下:
/**
添加元素
译:在此队列的尾部插入指定的元素,如果队列已满,则等待空间变得可用
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列满了: 队列中的元素数 count = items.length 当前队列条目数
while (count == items.length)
notFull.await(); // 注意:队列满了,进入阻塞等待
// 如果队列没满,则执行添加
enqueue(e);
} finally {
lock.unlock();
}
}
/**
移除元素
*/
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列为空,则进入阻塞等待
while (count == 0)
notEmpty.await();
// 如果队列不为空,执行移除元素操作
return dequeue();
} finally {
lock.unlock();
}
}
/**
移除具体操作
译:提取当前位置、前进和信号的元素。仅在持有锁定时调用。
*/
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
items index for next take, poll, peek or remove
// takeIndex:下一次移除的索引
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex]; // 通过索引获取当前要被移除的元素
items[takeIndex] = null; // 重置队列该索引位置 = null 清空元素
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 注意:上述队列已满put时,进入wait阻塞,这里通知唤醒了该condition
notFull.signal();
return x;
}
ArrayBlockingQueue 和 LinkedBlockingQueue区别:
ArrayBlockingQueue | LinkedBlockingQueue | |
---|---|---|
阻塞方式 | 通知模式 | 通知模式 |
是否有界 | 有界,须指定初始化大小 | 有界,构造器可以不初始化,默认为INTEGER.MAX_VALUE |
存取元素是否会创建和销毁 | 不会 | 会 |
生产者消费者锁 | 使用一把锁 | 各自使用不同的锁 |