1. 什么是阻塞队列?
阻塞队列(BlockingQueue) 是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
2. Java里的阻塞队列
JDK7提供了7个阻塞队列。分别是
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() | 不可用 | 不可用 |
抛出异常 与 返回特殊值 方法的实现是一样的,只不过对失败的操作的处理不一样!通过 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 文档提到的几点:
对于 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方法,是插入的队列的尾部),其他方法,操作的都是队列的头部(第一个元素)。
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();
}
2. LinkedBlockingQueue
LinkedBlockingQueue是一个用链表实现的 有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE。 此队列按照先进先出的原则对元素进行排序。
3. PriorityBlockingQueue
PriorityBlockingQueue是一个支持优先级的无界队列(虽然此队列逻辑上是无界的,但是资源被耗尽时试图执行 add 操作也将失败,导致 OutOfMemoryError)。默认情况下元素采取自然顺序排列(每个元素都必须实现 Comparable 接口),也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列.
其iterator() 方法中提供的迭代器并不 保证以特定的顺序遍历 PriorityBlockingQueue 的元素。如果需要 有序地进行遍历, 则应考虑使用 Arrays.sort(pq.toArray())。此外,可以使用方法 drainTo 按优先级顺序移除 全部或部分元素,并将它们放在另一个 collection 中。
在此类上进行的操作不保证具有同等优先级的元素的顺序。 如果需要实施某一排序,那么可以定义自定义类或者比较器,比较器可使用修改键断开主优先级值之间的联系。例如,以下是应用先进先出 (first-in-first-out) 规则断开可比较元素之间联系的一个类。要使用该类,则需要插入一个新的 FIFOEntry(anEntry) 来替换普通的条目对象。
class FIFOEntry<E extends Comparable super E>>
implements Comparable<FIFOEntry<E>> {
final static AtomicLong seq = new AtomicLong();
final long seqNum;
final E entry;
public FIFOEntry(E entry) {
seqNum = seq.getAndIncrement();
this.entry = entry;
}
public E getEntry() { return entry; }
public int compareTo(FIFOEntry other) {
int res = entry.compareTo(other.entry);
if (res == 0 && other.entry != this.entry)
res = (seqNum < other.seqNum ? -1 : 1);
return res;
}
}
4. DelayQueue
Delayed 元素的一个无界阻塞队列,只有在延迟期满时才能从中提取元素。注意 DelayQueue 的所有方法只能操作“到期的元素“,例如,poll()、remove()、size()等方法,都会忽略掉未到期的元素。
我们可以将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();
5. SynchronousQueue
一种阻塞队列,其中每个插入操作必须等待另一个线程的对应移除操作 ,反之亦然。
SynchronousQueue 的几个特点
//设置公平性的构造方法
public SynchronousQueue(boolean fair)
创建一个具有指定公平策略的 SynchronousQueue。
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。
原理:
参考自http://blog.csdn.net/xiaoxufox/article/details/52241317:
LinkedTransferQueue采用的一种预占模式。意思就是消费者线程取元素时,如果队列为空,那就生成一个节点(节点元素为null)入队,然后消费者线程park住,后面生产者线程入队时发现有一个元素为null的节点,生产者线程就不入队了,直接就将元素填充到该节点,唤醒该节点上park住线程,被唤醒的消费者线程拿货走人。
7. LinkedBlockingDeque
LinkedBlockingDeque是一个由链表结构组成的双向阻塞队列。所谓双向队列指的你可以从队列的两端插入和移出元素。 双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。 相比其他的阻塞队列,LinkedBlockingDeque多了addFirst,addLast,offerFirst,offerLast,peekFirst,peekLast等方法。另外,插入方法add等同于addLast,移除方法remove等效于removeFirst。但是take方法却等同于takeFirst,不知道是不是Jdk的bug,使用时还是用带有First和Last后缀的方法更清楚。
和 LinkedBlockingQueue 一样,是有界的阻塞队列,默认长度以及最大长度是 Integer.MAX_VALUE。可在创建时,指定容量。
如果队列是空的,消费者会一直等待,当生产者添加元素时候,消费者是如何知道当前队列有元素的呢?如果让你来设计阻塞队列你会如何设计,让生产者和消费者能够高效率的进行通讯呢?让我们先来看看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);
}
继续进入源码,发现调用setBlocker先保存下将要阻塞的线程,然后调用unsafe.park阻塞当前线程。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
unsafe.park(false, 0L);
setBlocker(t, null);
}
unsafe.park是个native方法,代码如下:
public native void park(boolean isAbsolute, long time);
park这个方法会阻塞当前线程,只有以下四种情况中的一种发生时,该方法才会返回。
我们继续看一下JVM是如何实现park方法的,park在不同的操作系统使用不同的方式实现,在linux下是使用的是系统方法pthread_cond_wait实现。实现代码在JVM源码路径src/os/linux/vm/os_linux.cpp里的 os::PlatformEvent::park方法,代码如下:
void os::PlatformEvent::park() {
int v ;
for (;;) {
v = _Event ;
if (Atomic::cmpxchg (v-1, &_Event, v) == v) break ;
}
guarantee (v >= 0, "invariant") ;
if (v == 0) {
// Do this the hard way by blocking ...
int status = pthread_mutex_lock(_mutex);
assert_status(status == 0, status, "mutex_lock");
guarantee (_nParked == 0, "invariant") ;
++ _nParked ;
while (_Event < 0) {
status = pthread_cond_wait(_cond, _mutex);
// for some reason, under 2.7 lwp_cond_wait() may return ETIME ...
// Treat this the same as if the wait was interrupted
if (status == ETIME) { status = EINTR; }
assert_status(status == 0 || status == EINTR, status, "cond_wait");
}
-- _nParked ;
// In theory we could move the ST of 0 into _Event past the unlock(),
// but then we'd need a MEMBAR after the ST.
_Event = 0 ;
status = pthread_mutex_unlock(_mutex);
assert_status(status == 0, status, "mutex_unlock");
}
guarantee (_Event >= 0, "invariant") ;
}
}
pthread_cond_wait是一个多线程的条件变量函数,cond是condition的缩写,字面意思可以理解为线程在等待一个条件发生,这个条件是一个全局变量。这个方法接收两个参数,一个共享变量_cond,一个互斥量_mutex。而unpark方法在linux下是使用pthread_cond_signal实现的。park 在windows下则是使用WaitForSingleObject实现的。
当队列满时,生产者往阻塞队列里插入一个元素,生产者线程会进入WAITING (parking)状态。我们可以使用jstack dump阻塞的生产者线程看到这点:
"main" prio=5 tid=0x00007fc83c000000 nid=0x10164e000 waiting on condition [0x000000010164d000]
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x0000000140559fe8> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
at java.util.concurrent.ArrayBlockingQueue.put(ArrayBlockingQueue.java:324)
at blockingqueue.ArrayBlockingQueueTest.main(ArrayBlockingQueueTest.java:11)