转《Java并发编程的艺术》
1.什么是阻塞队列
阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作支持阻塞
的插入和移除方法。
1)支持阻塞的插入方法:意思是当队列满时,队列会阻塞插入元素的线程,直到队列不
满。
2)支持阻塞的移除方法:意思是在队列为空时,获取元素的线程会等待队列变为非空。
阻塞队列常用于生产者和消费者的场景,生产者是向队列里添加元素的线程,消费者是
从队列里取元素的线程。阻塞队列就是生产者用来存放元素、消费者用来获取元素的容器。
2.Java里的阻塞队列
JDK7提供了7个阻塞队列。分别是
- ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
- DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue: 一个不存储元素的阻塞队列。
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
3.BlockingQueue接口 与 BlockingDeque 接口
JDK提供的阻塞队列中,LinkedBlockingDeque 是一个 Deque(双向的队列),其实现的接口是 BlockingDeque;其余6个阻塞队列则是 Queue(单向队列),实现的接口是 BlockingQueue。
对于 BlockingQueue 的阻塞队列提供了四种处理方法:
方法描述 | 抛出异常 | 返回特殊的值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入数据 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
获取并移除队列的头 | remove() | poll() | take() | poll(time,unit) |
获取但不移除队列的头 | element() | peek() | 不可用 | 不可用 |
- 抛出异常: 是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementEx·ception异常 。
- 返回特殊值: 插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
- 一直阻塞: 当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
-
超时退出: 当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。
抛出异常 与 返回特殊值 方法的实现是一样的,只不过对失败的操作的处理不一样!通过 AbstractQueue 的源码可以发现,add(e),remove(),element() 都是分别基于 offer(),poll(),peek() 实现的
public boolean add(E arg0) {
if (this.offer(arg0)) {
return true;
} else {
throw new IllegalStateException("Queue full");
}
}
public E remove() {
Object arg0 = this.poll();
if (arg0 != null) {
return arg0;
} else {
throw new NoSuchElementException();
}
}
public E element() {
Object arg0 = this.peek();
if (arg0 != null) {
return arg0;
} else {
throw new NoSuchElementException();
}
}
JDK 文档提到的几点:
- BlockingQueue 不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。
- BlockingQueue 可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put 附加元素。没有任何内部容量约束的 BlockingQueue 总是报告 Integer.MAX_VALUE 的剩余容量。
- BlockingQueue 实现主要用于生产者-使用者队列,但它另外还支持 Collection 接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常不 会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。
-
BlockingQueue 实现是线程安全的
。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。然而,大量的 Collection 操作(addAll、containsAll、retainAll 和 removeAll,这些方法尽可能地少使用)没有 必要自动执行,除非在实现中特别说明。因此,举例来说,在只添加了 c 中的一些元素后,addAll(c) 有可能失败(抛出一个异常)。
对于 BlockingDeque 的双向队列也提供了四种形式的方法
第一个元素(头部)
方法描述 | 抛出异常 | 返回特殊的值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入数据 | addFirst(e) | offerFirst(e) | putFirst(e) | offerFirst(e, time, unit) |
获取并移除队列的头 | removeFirst() | pollFirst() | takeFirst() | pollFirst(time, unit) |
获取但不移除队列的头 | getFirst() | peekFirst() | 不可用 | 不可用 |
最后一个元素(尾部)
方法描述 | 抛出异常 | 返回特殊的值 | 一直阻塞 | 超时退出 |
---|---|---|---|---|
插入数据 | addLast(e) | offerLast(e) | putLast(e) | offerLast(e, time, unit) |
获取并移除队列的头 | removeLast() | pollLast() | takeLast() | pollLast(time, unit) |
获取但不移除队列的头 | getLast() | peekLast() | 不适用 | 不适用 |
像所有 BlockingQueue 一样,BlockingDeque 是线程安全的,但不允许 null 元素,并且可能有(也可能没有)容量限制。
BlockingDeque 接口继承扩展了 BlockingQueue 接口,对于 继承自 BlockingQueue 的方法,除了插入方法(add、poll、offer方法,是插入的队列的尾部),其他方法,操作的都是队列的头部(第一个元素)。
4.七个阻塞队列的详细介绍
4.1 ArrayBlockingQueue
ArrayBlockingQueue是一个用数组实现的 有界阻塞队列。 此队列按照先进先出(FIFO)的原则对元素进行排序。
默认情况下不保证访问者公平地访问队列
,所谓公平访问队列是指阻塞的线程,可按照阻塞的先后顺序访问队列。非公平性是对先等待的线程是不公平的,当队列可用时,阻塞的线程都可以竞争访问队列的资格。
为了保证公平性,通常会降低吞吐量。
ArrayBlockingQueue fairQueue = new ArrayBlockingQueue(1000,true);
访问者的公平性是使用可重入锁实现的 ,代码如下:
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
4.2 LinkedBlockingQueue
LinkedBlockingQueue是一个用链表实现的 有界阻塞队列。此队列的默认的最大长度为Integer.MAX_VALUE
。 此队列按照先进先出的原则对元素进行排序。
4.3 PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采取自然顺序
升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化
PriorityBlockingQueue时,指定构造参数Comparator来对元素进行排序。需要注意的是不能保证同优先级元素的顺序。
4.4 DelayQueue
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。注意 DelayQueue 的所有方法只能操作“到期的元素“,例如,poll()、remove()、size()等方法,都会忽略掉未到期的元素。
我们可以将DelayQueue运用在以下应用场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。
DelayQueue
的实现是基于 PriorityQueue
,是一个优先级队列,是以延时时间的长短进行排序的。所以,DelayQueue
需要知道每个元素的延时时间,而这个延时时间是由 Delayed 接口的 getDelay()方法获取的。所以, DelayQueue
的元素必须实现Delay 接口
;
//计算并返回延时时间
public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), TimeUnit.NANOSECONDS);
}
延时队列的原理
延时队列的实现很简单,当消费者从队列里获取元素时,如果元素没有达到延时时间,就阻塞当前线程。
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay <= 0)
return q.poll();
else if (leader != null)
available.await();
4.5 SynchronousQueue
SynchronousQueue是一个不存储元素的阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。
它支持公平访问队列。默认情况下线程采用非公平性策略访问队列。使用以下构造方法
可以创建公平性访问的SynchronousQueue,如果设置为true,则等待的线程会采用先进先出的顺序访问队列。
不像ArrayBlockingQueue或LinkedListBlockingQueue,SynchronousQueue内部并没有数据缓存空间,你不能调用peek()方法来看队列中是否有数据元素,因为数据元素只有当你试着取走的时候才可能存在,不取走而只想偷窥一下是不行的,当然遍历这个队列的操作也是不允许的。队列头元素是第一个排队要插入数据的线程,而不是要交换的数据。
数据是在配对的生产者和消费者线程之间直接传递的,并不会将数据缓冲数据到队列中。可以这样来理解:生产者和消费者互相等待对方,握手,然后一起离开。
使用示例:
package com.dxz.queue.block;
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueDemo {
public static void main(String[] args) throws InterruptedException {
final SynchronousQueue<Integer> queue = new SynchronousQueue<Integer>();
Thread putThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("put thread start");
try {
queue.put(1);
} catch (InterruptedException e) {
}
System.out.println("put thread end");
}
});
Thread takeThread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("take thread start");
try {
System.out.println("take from putThread: " + queue.take());
} catch (InterruptedException e) {
}
System.out.println("take thread end");
}
});
putThread.start();
Thread.sleep(1000);
takeThread.start();
}
}
4.6 LinkedTransferQueue
LinkedTransferQueue是一个由链表结构组成的 无界阻塞TransferQueue队列 。相对于其他阻塞队列LinkedTransferQueue多了tryTransfer和transfer方法。
transfer方法: 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。transfer方法的关键代码如下:
Node pred = tryAppend(s, haveData);
return awaitMatch(s, pred, e, (how == TIMED), nanos);
tryTransfer方法: 则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。和transfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
对于带有时间限制的 tryTransfer(E e, long timeout, TimeUnit unit)方法 ,则是试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true。
4.7 LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。 双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后缀的方法更清楚。
和 LinkedBlockingQueue 一样,是有界的阻塞队列,默认长度以及最大长度是 Integer.MAX_VALUE。可在创建时,指定容量。
5 阻塞队列的实现原理
如果队列是空的,消费者会一直等待,当生产者添加元素时候,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,让生产者和消费者能够高效率的进行通讯呢?让我们先来看看JDK是如何实现的。
使用通知模式实现。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞住生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。通过查看JDK源码发现ArrayBlockingQueue使用了Condition来实现,代码如下
private final Condition notFull;
private final Condition notEmpty;
public ArrayBlockingQueue(int capacity, boolean fair) {
//省略其他代码
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
}
}
private void insert(E x) {
items[putIndex] = x;
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
当我们往队列里插入一个元素时,如果队列不可用,阻塞生产者主要通过LockSupport.park(this);来实现
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}