认识BlockingQueue
阻塞队列,顾名思义,首先它是一个队列,而一个队列在数据结构中所起的作用大致如下图所示:
从上图我们可以很清楚看到,通过一个共享的队列,可以使得数据由队列的一端写入,从另外一端取出;
可以看到,BlockingQueue和List、Set接口一样是继承于Collection接口;
简要概述BlockingQueue常用的四个实现类
- 1、ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的.
- 2、LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的
- 3、PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序.
- 4、SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的(必须存一个,就等待取出后才能再存).
- 其中LinkedBlockingQueue和ArrayBlockingQueue比较起来,它们背后所用的数据结构不一样,导致LinkedBlockingQueue的数据吞吐量要大于ArrayBlockingQueue,但在线程数量很大时其性能的可预见性低于ArrayBlockingQueue.
BlockingQueue的四组API
抛出异常 | 不抛出异常,返回一个值 | 阻塞(一直等待) | 阻塞(超时不等) | |
---|---|---|---|---|
写入 | add() | offer() | put() | offer(obj,time,timeUnit) |
取出 | remove() | poll() | take() | poll(time,timeUnit) |
检查队首 | element() | peek() | - | - |
1、抛出异常
//抛出异常
public static void test01(){
//创建队列,设置队列大小
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
//设置三次循环写入
for (int i = 0; i < 3; i++) {
System.out.println("插入数据"+i+": "+queue.add(i));
}
queue.add(9); //当队列满后再次存入
//设置三次循环取出
for (int i = 0; i < 3; i++) {
System.out.println("移除数据"+i+": "+queue.remove(i));
}
}
运行结果
可以看到,我们运行这个方法后,程序抛出了一个异常,提示我们队列已满;
我们查看add方法的源码:
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and throwing an
* {@code IllegalStateException} if this queue is full.
*
* @param e the element to add
* @return {@code true} (as specified by {@link Collection#add})
* @throws IllegalStateException if this queue is full
* @throws NullPointerException if the specified element is null
*/
public boolean add(E e) {
return super.add(e);
}
...
/** 以下方法介绍的是super.add(e)
* Inserts the specified element into this queue if it is possible to do so
* immediately without violating capacity restrictions, returning
* <tt>true</tt> upon success and throwing an <tt>IllegalStateException</tt>
* if no space is currently available.
*
* <p>This implementation returns <tt>true</tt> if <tt>offer</tt> succeeds,
* else throws an <tt>IllegalStateException</tt>.
*
* @param e the element to add
* @return <tt>true</tt> (as specified by {@link Collection#add})
* @throws IllegalStateException if the element cannot be added at this
* time due to capacity restrictions
* @throws ClassCastException if the class of the specified element
* prevents it from being added to this queue
* @throws NullPointerException if the specified element is null and
* this queue does not permit null elements
* @throws IllegalArgumentException if some property of this element
* prevents it from being added to this queue
*/
public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}
在add方法中,介绍了,若队列已满,则会抛出一个异常表示队列已满;
2、不抛出异常,返回一个值
//不抛出异常,返回一个值
public static void test02(){
//创建队列,设置队列大小
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
//设置三次循环写入
for (int i = 0; i < 3; i++) {
System.out.println("插入数据"+i+": "+queue.offer(i));
}
System.out.println(queue.offer(9));
//设置三次循环取出
for (int i = 0; i < 3; i++) {
System.out.println("移除数据"+i+": "+queue.poll());
}
System.out.println(queue.poll());
}
运行结果
可以看到,当队列已满,我们再向队列写入数据时,程序不会再出现异常,而是返回了一个false,告诉我们写入失败;而当队列为空,我们再从队列中移除数据,程序也会返回一个null,告诉我们队列已无法再取出值;
我们查看一下offer方法和poll方法的源码:
/**
* Inserts the specified element at the tail of this queue if it is
* possible to do so immediately without exceeding the queue's capacity,
* returning {@code true} upon success and {@code false} if this queue
* is full. This method is generally preferable to method {@link #add},
* which can fail to insert an element only by throwing an exception.
*
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
在offer方法和poll方法中,对异常进行了处理;
3、一直等待
//一直等待
public static void test03() throws InterruptedException {
//创建队列,设置队列大小
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
//设置三次循环写入
for (int i = 0; i < 3; i++) {
System.out.println("写入第"+i+"个数据");
queue.put(i);
}
//当队列已满,继续写入
queue.put(9);
//设置三次循环取出
for (int i = 0; i < 3; i++) {
System.out.println("移除数据"+i+": "+queue.take());
}
//当队列已空,继续取出
System.out.println(queue.take());
}
运行结果
可以看到,程序进入阻塞状态,由于队列已满,程序就会一直等待队列有空位时进行入队操作;
我们来查看一下put方法的源码:
/**
* Inserts the specified element at the tail of this queue, waiting
* for space to become available if the queue is full.
*
* @throws InterruptedException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
在put方法中,很明显能看到一个await()方法;
4、超时不等
//超时不等
public static void test04() throws InterruptedException {
//创建队列,设置队列大小
ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<>(3);
//设置三次循环写入
for (int i = 0; i < 3; i++) {
System.out.println("插入数据"+i+": "+queue.offer(i));
}
System.out.println(queue.offer(9,2,TimeUnit.SECONDS));
//设置三次循环取出
for (int i = 0; i < 3; i++) {
System.out.println("移除数据"+i+": "+queue.poll());
}
System.out.println(queue.poll(2,TimeUnit.SECONDS));
}
在此处,我们采用了和 2 中
不抛出异常,返回一个值
中一样的方法offer
和poll
,只不过我们使用的是这两个方法的另一个重载方法offer(参数1,参数2,参数3)
,poll(参数1,参数2)
,其中offer()方法和poll()方法中新增的两个参数分别代表的是时间长度和时间单位
。在这个案例中,我们采用的是等待2秒的操作,当队列已满时,再向队列中插入数据,新插入的数据就会等待2秒中,如果这2秒内队列有空位可入队,那么就会执行入队操作,否则就会放弃入队,返回一个false标识入队失败。同样poll方法也是等待2秒,若超过2秒无数据可取出,就会返回一个null。
SynchronousQueue
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
简单来说,就是该队列只有将入队后的数据被取出后才会再继续执行入队操作,否则一直阻塞。
存取操作:
- put(); 存入
- take(); 取出
public class SynchronousQueueDemo {
public static void main(String[] args) {
SynchronousQueue<Object> synchronousQueue = new SynchronousQueue<>();
new Thread(()->{
try {
System.out.println(Thread.currentThread().getName()+"生产1");
synchronousQueue.put(1);
System.out.println(Thread.currentThread().getName()+"生产2");
synchronousQueue.put(2);
System.out.println(Thread.currentThread().getName()+"生产3");
synchronousQueue.put(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
},"product").start();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"消费"+synchronousQueue.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"消费"+synchronousQueue.take());
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+"消费"+synchronousQueue.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
},"consumer").start();
}
}
运行结果
可以看到,每次产与消费都是一一对应的,只有生产的被消费了才能继续写入队列。