BlockingQueue(阻塞队列)分析
如果读者还有一点印象,我们在实现线程池时,用了队列这种数据结构来存储接收到的任务,在多线程环境中阻塞队列是一种非常有用的队列,在介绍BlockingQueue之前,我们先解释一下Queue接口。
Queue接口
boolean offer(E e); 将指定的元素插入此队列,当使用有容量限制的队列时,此方法通常要优于add(E),果该元素已添加到此队列,则返回true;否则返回false
E peek(); 获取但不移除此队列的头元素;如果此队列为空,则返回 null。
E poll(); 获取并移除此队列的头元素,如果此队列为空,则返回 null。
boolean add(E e); 将指定的元素插入此队列,在成功时返回 true,如果当前没有可用的空间,则抛出 IllegalStateException
E element(); 获取,但是不移除此队列的第一个元素。此方法与 peek 唯一的不同在于:此队列为空时将抛出NoSuchElementException异常
E remove(); 获取,并移除此队列的第一个元素。此方法与 poll唯一的不同在于:此队列为空时将抛出NoSuchElementException异常
AbstractQueue是Queue实现的抽象类,实现了Queue的大部分函数,只有三个函数offer、peek和poll没有实现。
BlockingQueue接口
BlockingQueue接口继承了Queue接口,它还定义了下面一些操作:
void put(E e) throws InterruptedException; 将指定元素插入此队列中,将等待可用的空间,当等待时可以被中断。
E take() throws InterruptedException; 获取并移除此队列的头元素,在元素变得可用之前一直等待,当等待时,也可以被中断。
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException; 将指定元素插入此队列中,在到达指定的等待时间前等待可用的空间,timeout为等待时间,如果成功,则返回 true;如果在空间可用前超过了指定的等待时间,则返回 false,当等待时可以被中断。
E poll(long timeout, TimeUnit unit) throws InterruptedException; 获取并移除此队列的头元素,可以在指定的等待时间前等待可用的元素,timeout表明放弃之前要等待的时间长度,用 unit 的时间单位表示,如果在元素可用前超过了指定的等待时间,则返回null,当等待时可以被中断。
int remainingCapacity(); BlockingQueue是限定容量的,它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put 附加元素。没有任何内部容量约束的 BlockingQueue 总是报告 Integer.MAX_VALUE 的剩余容量。
int drainTo(Collection<? super E> c); 移除此队列中所有可用的元素,并将它们添加到给定collection 中。此操作可能比反复轮询此队列更有效。在试图向 collection c 中添加元素没有成功时,可能导致在抛出相关异常时。如果试图将一个队列放入自身队列中,则会导致 IllegalArgumentException异常。此外,如果正在进行此操作时修改指定的 collection,则此操作行为是不确定的
int drainTo(Collection<? super E> c, int maxElements); 最多从此队列中移除给定数量的可用元素,并将这些元素添加到给定collection中,别的行为与上面int drainTo(Collection<? super E> c)一样。
BlockingQueue方法以四种形式出现,对于不能立即满足但可能在将来某一时刻可以满足的操作,这四种形式的处理方式不同:第一种是抛出一个异常,第二种是返回一个特殊值(null 或 false,具体取决于操作),第三种是在操作可以成功前,无限期地阻塞当前线程,第四种是在放弃前只在给定的最大时间限制内阻塞。下表中总结了这些方法:
|
抛出异常 |
特殊值 |
阻塞 |
超时 |
插入 |
Add(e) |
offer(e) |
put(e) |
offer(e, time, unit) |
删除 |
Remove() |
poll() |
take() |
poll(time, unit) |
检查 |
Element() |
peek() |
不可用 |
不可用 |
下面面依次介绍几种典型的阻塞队列。
ArrayBlockingQueue
ArrayBlockingQueue是一个以数组为存储空间的有界阻塞队列。此队列按 FIFO(先进先出)原则对元素进行排序。队列的头部是在队列中存在时间最长的元素,队列的尾部 是在队列中存在时间最短的元素。新元素插入到队列的尾部,队列获取操作则是从队列头部开始获得元素。这是一个典型的“有界缓存区”,固定大小的数组在其中保持生产者插入的元素和使用者提取的元素。一旦创建了这样的缓存区,就不能再增加其容量。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致阻塞。
此类支持对等待的生产者线程和消费者线程进行排序的可选公平策略。默认情况下,不保证是这种排序。通过将公平性 (fairness) 设置为 true 而构造的队列允许按照 FIFO 顺序访问线程。公平性通常会降低吞吐量,但也减少了可变性和避免了“不平衡性”。
内部实现
private final E[] items;//存储数组,一旦设定,不能改变
private int takeIndex;//检查或输出元素的位置
private int putIndex;//添加元素的位置
private int count;//元素的个数
private final ReentrantLock lock;//锁
private final Condition notEmpty;//队列空,采用的信号量
private final Condition notFull;//队列满,采用的信号量
ArrayBlockingQueue构造函数
ArrayBlockingQueue也提供了几个构造函数,我们看其中几个:
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = (E[]) new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
Capacity参数表明队列最多能存储的元素
Fair参数是否创建可重入公平锁
ArrayBlockingQueue的构造函数,首先创建一个存储缓冲区,然后创建一把锁好两个条件变量。
ArrayBlockingQueue的offer函数
Offer函数通常都是先加锁,然后判断队列是否为满,如果不满,直接插入到队尾,否则可能等待,或者返回false,或者抛异常,最后释放锁。
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();//加锁
try {
if (count == items.length)
return false;//满时,直接返回false
else {
insert(e);//添加到队尾
return true;
}
} finally {
lock.unlock();//解锁
}
}
带有等待时间的offer函数通常会调用条件变量的awaitNanos函数
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();//可中断加锁
try {
for (;;) {
if (count != items.length) {
insert(e);//可以在队尾插入元素
return true;
}
if (nanos <= 0)
return false; //反正时间到,直接返回false
try {
nanos = notFull.awaitNanos(nanos);//等待,知道返回为<=0
} catch (InterruptedException ie) {
notFull.signal(); // 中断是,唤醒别他线程
throw ie;
}
}
} finally {
lock.unlock();
}
}
ArrayBlockingQueue的peek函数
Peek函数比较简单,先加锁,再判断是否为空,空时,放回null,否则,返回头元素。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : items[takeIndex];
} finally {
lock.unlock();
}
}
ArrayBlockingQueue的poll函数
Poll有两个函数,一个不带参数,另外一个带等待时间参数,不带参数的poll比较简单,如下面的源代码
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == 0)
return null;//空时,返回null
E x = extract();//操作队列的头部,返回头部
return x;
} finally {
lock.unlock();
}
}
带等待时间参数poll函数,跟带有时间参数的offer函数,复杂度差不多,都是调用条件变量的等待函数,如下面的源代码:
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
if (count != 0) {
E x = extract();
return x;
}
if (nanos <= 0)
return null;
try {
nanos = notEmpty.awaitNanos(nanos);
} catch (InterruptedException ie) {
notEmpty.signal(); //
throw ie;
}
}
} finally {
lock.unlock();
}
}
ArrayBlockingQueue需要用户事先知道队列的大小,而且容量需要设定,因此在使用时,需要特别注意这一点,下面介绍的LinkedBlockingQueue是个没有界限的阻塞队列,应用的场景比界限的ArrayBlockingQueue多。
LinkedBlockingQueue
LinkedBlockingQueue是一个基于已链接节点的、范围任意的blocking queue的实现。 此队列按 FIFO(先进先出)排序元素,队列的头部是在队列中时间最长的元素,队列的尾部 是在队列中时间最短的元素。 新元素插入到队列的尾部,并且队列检索操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列。
内部实现
LinkedBlockingQueue定义了Node节点存储数据元素,Node就是一个普通的链表节点
static class Node<E> {
E item;
Node<E> next;
Node(E x) { item = x; }
}
LinkedBlockingQueue使用下面的数据变量来实现阻塞队列
private final int capacity;//队列的最大容量
private final AtomicInteger count = new AtomicInteger(0);//队列大小
private transient Node<E> head;//队列头
private transient Node<E> last;//队列尾
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
LinkedBlockingQueue中读只操作队头,而写只操作队尾,因此巧妙地采用了两把锁,对put和offer采用putLock,对take和poll采用takeLock,避免了读写时相互竞争锁的现象,因此LinkedBlockingQueue在高并发读写操作都多的情况下,性能会比ArrayBlockingQueue好很多,在遍历以及删除元素时则要锁住两把锁。
LinkedBlockingQueue构造函数
LinkedBlockingQueue提供了几个构造函数,我们这里列举其中两个:
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);//默认容量上限为 Integer.MAX_VALUE
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
LinkedBlockingQueue的offer函数
先看不带时间参数的offer,它先判断队列是否满,空时返回false,再添加元素,判断队列未添加之前,是否为空,如果是,唤醒需要取得线程。
public boolean offer(E e) {
if (e == null) throw new NullPointerException();//e==null
final AtomicInteger count = this.count;
if (count.get() == capacity)//队里满
return false;
int c = -1;
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {//添加到队列
enqueue(e);
c = count.getAndIncrement();
if (c + 1 < capacity)//没有满时
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)//队列空时,唤醒取线程
signalNotEmpty();
return c >= 0;
}
带时间参数的offer,逻辑上与上面的offer函数相似,多采用了一个while循环,在循环里,等待指定的时间。
public boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
long nanos = unit.toNanos(timeout);
int c = -1;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
enqueue(e);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return true;
}
LinkedBlockingQueue的peek函数
Peek函数在加锁后,直接取头元素,请看下面的代码
public E peek() {
if (count.get() == 0)
return null;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
Node<E> first = head.next;
if (first == null)
return null;
else
return first.item;
} finally {
takeLock.unlock();
}
}
LinkedBlockingQueue的poll函数
poll函数与offer函数恰好相反,代码逻辑相反
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
带有等待时间的poll代码采用等待指定的时间的await函数,代码很容易懂。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
E x = null;
int c = -1;
long nanos = unit.toNanos(timeout);
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
if (nanos <= 0)
return null;
nanos = notEmpty.awaitNanos(nanos);
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
LinkedBlockingDeque(双向并发阻塞队列)
LinkedBlockingDeque是双向并发阻塞队列,所谓双向是指可以从队列的头和尾同时操作,并发只是线程安全的实现,阻塞允许在入队出队不满足条件时挂起线程,这里说的队列是指支持FIFO/FILO实现的链表。
LinkedBlockingDeque实现了BlockingDeque接口,我们看了BlockingDeque中的几个方法,就能明白LinkedBlockingDeque的功能
boolean offerFirst(E e, long timeout, TimeUnit unit) throws InterruptedException;
boolean offerLast(E e, long timeout, TimeUnit unit) throws InterruptedException;
E pollFirst(long timeout, TimeUnit unit) throws InterruptedException;
E pollLast(long timeout, TimeUnit unit) throws InterruptedException;
我们再来看一下它的成员变量:
static final class Node<E> {
E item;
Node<E> prev;
Node<E> next;
Node(E x, Node<E> p, Node<E> n) {
item = x;
prev = p;
next = n;
}
}
transient Node<E> first;
transient Node<E> last;
private transient int count;
private final int capacity;
final ReentrantLock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
查看这些数据结构,可以得到以下结论:
(1)要想支持阻塞功能,队列的容量一定是固定的,否则无法在入队的时候挂起线程。也就是capacity是final类型的。
(2)既然是双向链表,每一个结点就需要前后两个引用,这样才能将所有元素串联起来,支持双向遍历,也即prev/next两个引用。
(3)双向链表需要头尾同时操作,需要first/last两个节点。
(4)既然要支持阻塞功能,就需要锁和条件变量来挂起线程。这里使用一个锁两个条件变量来完成此功能。
前面分析linkedBlockingQueue实现后,LinkedBlockingDeque的原理和实现就不值得一提了,无非是在独占锁下对一个链表的普通操作,我们这里就不分析源代码了。
LinkedBlockingDeque优点当然是功能足够强大,同时由于采用一个独占锁,因此实现起来也比较简单。所有对队列的操作都加锁就可以完成。同时独占锁也能够很好的支持双向阻塞的特性。缺点就是由于独占锁,所以不能同时进行两个操作,这样性能上就大打折扣。从性能的角度讲LinkedBlockingDeque要比LinkedBlockingQueue要低很多,比CocurrentLinkedQueue就低更多了,这在高并发情况下就比较明显了。
PriorityBlockingQueue(优先阻塞队列)
PriorityBlockingQueue是个一个无界的阻塞队列,它使用与类 PriorityQueue 相同的顺序规则,并且提供了阻塞检索的操作。虽然此队列逻辑上是无界的,但是由于资源被耗尽,所以试图执行添加操作可能会失败(导致 OutOfMemoryError),此类不允许使用 null 元素。
PriorityBlockingQueue的实现比较简单,我们稍微看看,就能了解它是怎么实现的,因为它是无价的,不需要notFull这个条件变量
private final PriorityQueue<E> q;
private final ReentrantLock lock = new ReentrantLock(true);
private final Condition notEmpty = lock.newCondition();
offer、peek和poll函数也相当简单,这里也贴出它们的代码,
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
boolean ok = q.offer(e);
assert ok;
notEmpty.signal();
return true;
} finally {
lock.unlock();
}
}
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.peek();
} finally {
lock.unlock();
}
}
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.poll();
} finally {
lock.unlock();
}
}
SynchronousQueue
SynchronousQueue也是一种阻塞队列,其中每个 put 必须等待一个 take,同步队列没有任何内部容量,甚至连一个队列的容量都没有。不能在同步队列上进行 peek,因为仅在试图要取得元素时,该元素才存在;除非另一个线程试图移除某个元素,否则也不能(使用任何方法)添加元素;也不能迭代队列,因为其中没有元素可用于迭代。队列的头 是尝试添加到队列中的首个已排队线程元素;如果没有已排队线程,则不添加元素并且头为 null。
这里就不详细介绍它的原理
DelayQueue
DelayQueue是一个无界阻塞队列,只有在延迟期满时才能从中提取元素。该队列的头部是延迟期满后保存时间最长的 Delayed 元素。如果延迟都还没有期满,则队列没有头部,并且poll 将返回 null。当一个元素的getDelay(TimeUnit.NANOSECONDS) 方法返回一个小于或等于零的值时,则出现期满。此队列不允许使用 null 元素。
DelayQueue中的元素必须实现Delayed接口
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
DelayQueue定义了下面几个变量实现阻塞队列:
private transient final ReentrantLock lock = new ReentrantLock();
private transient final Condition available = lock.newCondition();
private final PriorityQueue<E> q = new PriorityQueue<E>();
我们稍微解释了DelayQueue中的offer、peek和poll函数
带等待时间参数和不带等待时间参数的offer实现是一样的,在队列为空的时候,唤醒线程。
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
q.offer(e);
if (first == null || e.compareTo(first) < 0)
available.signalAll();
return true;
} finally {
lock.unlock();
}
}
Peek函数返回第一个元素,返回的元素不一定符合延时要求。
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.peek();
} finally {
lock.unlock();
}
}
Poll函数在拿出元素后,会判断该元素是否符合延时要求,当getDelay<=0时,符合要求,否则返回null
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
if (first == null || first.getDelay(TimeUnit.NANOSECONDS) > 0)
return null;
else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll();
return x;
}
} finally {
lock.unlock();
}
}
带等待时间参数的poll也差不多,只是要多考虑timeout参数,如果元素的延时在timeout之前,则返回该元素,否则返回null。
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();
if (first == null) {
if (nanos <= 0)
return null;
else
nanos = available.awaitNanos(nanos);
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay > 0) {
if (nanos <= 0)
return null;
if (delay > nanos)
delay = nanos;
long timeLeft = available.awaitNanos(delay);
nanos -= delay - timeLeft;
} else {
E x = q.poll();
assert x != null;
if (q.size() != 0)
available.signalAll();
return x;
}
}
}
} finally {
lock.unlock();
}
}
总结一下阻塞队列
(1)ArrayBlockingQueue:规定大小的BlockingQueue,其构造函数必须带一个int参数来指明其大小.其所含的对象是以FIFO(先入先出)顺序排序的.
(2)LinkedBlockingQueue:大小不定的BlockingQueue,若其构造函数带一个规定大小的参数,生成的BlockingQueue有大小限制,若不带大小参数,所生成的BlockingQueue的大小由Integer.MAX_VALUE来决定.其所含的对象是以FIFO(先入先出)顺序排序的
(3)PriorityBlockingQueue:类似于LinkedBlockQueue,但其所含对象的排序不是FIFO,而是依据对象的自然排序顺序或者是构造函数的Comparator决定的顺序.
(4)SynchronousQueue:特殊的BlockingQueue,对其的操作必须是放和取交替完成的.
posted on 2014-05-14 10:30 cloudkiller 阅读(3876) 评论(0) 编辑 收藏 举报