前言
最近一直在看队列相关的源码,上一篇介绍了优先队列的实现,这一篇则看一看延迟队列是如何实现。优先队列主要是分析其数据结构的实现,而延迟队列不一样。延迟队列的底层其实就是优先队列,它利用优先队列排序的功能将插入的元素按照等待时间先后保存起来,然后取出最先等待时间到期的元素。所以,延迟队列主要是分析其如何实现延迟的,又是如何实现线程安全的。
简述
- 接口定义
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E>
这是 DelayQueue 的接口定义,可见其保存的元素必须是 Delayed 类型的。
public interface Delayed extends Comparable<Delayed> {
/**
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}
所以延迟队列保存的每一个元素必须继承 Delayed 类,实现 getDelay() 方法,getDalay() 方法设置该元素延迟的时间。
- 数据结构
private final PriorityQueue<E> q = new PriorityQueue<E>();
DelayQueue 保存的每一个元素都是保存在优先队列 q 中的,增删改查实则都是对 q 的增删改查。
- 执行线程
private Thread leader = null;
leader 保存正在执行的线程。其它线程发现 leader 不为 null 时,则阻塞。这是 DelayQueue 实现的一个亮点,为啥是亮点,在后面会细说。
- 锁
private final transient ReentrantLock lock = new ReentrantLock();
private final Condition available = lock.newCondition();
DelayQueue 没有采用 synchronized 锁,而是采用了 ReentrantLock 锁,ReentrantLock 能够实现定时并可中断。要理解 DelayQueue 是如何实现线程安全的,必须掌握 ReentrantLock 锁的相关知识,这里对 ReentrantLock 锁进行简单的介绍,列举了在 DelayQueue 源码使用到的 Lock 接口的功能。
ReentrantLock
在 Java5.0 之前,协调共享对象访问可以使用的机制只有 Synchronized 和 Volatile。Java5.0 新增了一种新的锁机制:ReentrantLock 。:ReentrantLock 与 Synchronized 锁相比,具有更大的灵活性。ReentrantLock 提供了一种无条件的、可轮询的、定时的以及可中断的锁操作。Synchronized 锁在同步代码执行结束后会自动释放锁,Lock 则需要自己释放锁,所以用 Lock 实现代码同步时最后一定要 unlock() 释放锁。
Lock lock = new ReentrantLock();
lock.lock();
try{
// 同步代码块
} finally {
lock.unlock(); //释放锁
}
我们知道,synchronized 提供了 wait() 和 notify() 方法实现了线程间的等待/通知功能。ReentrantLock 同样可以实现,不过需要借助 Condition 对象。
Lock lock = new ReentrantLock();
lock.lock();
Condition conditon = lock.newCondition();
public void await(){
try{
lock.lock();
//当前线程阻塞
condition.await();
} finally{
lock.unlock();
}
}
public void singnal(){
try{
lock.lock();
//唤醒阻塞线程
condition.signal();
} finally{
lock.unlock();
}
}
利用 Condition 的awiat() 和 signal() 实现线程的阻塞和唤醒。
同时,Lock 提供了几个重要接口,实现了锁的中断,定时等功能。
- lockInterrruptibly(): 如果当前线程未被中断,则获取锁。如果被中断,则抛出异常。
Lock lock = new ReentrantLock();
public void method(){
try{
lock.lockInterrruptibly();
...
} finally {
lock.unlock();
}
}
如果线程 thread 执行 method 方法,在等待获取锁时,线程 thread 被 interrupt() 中断,那么thread 会抛出异常,或者 thread 早已被 interrupt 打上中断的标记,在进入 lockInterrruptibly 获取锁时,同样会抛出异常。
tryLock():如果锁未被其它线程保持,则获取该锁,并立即返回 true,否则立即发回 false。lock() 方法不同,它在获取不到的锁的时候会阻塞直到取到锁。
tryLock(long timeout, TImeUnit unit):如果锁没有被其它线程保持,则立即获取到锁并返回 true。否则等待timeout,如果还没获取到锁,则返回 false.
take
take() 方法是 DelayQueue 最为常用的接口,它能够阻塞线程,直到队列有到期元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //阻塞直到取到锁。如果等待中线程标记中断,则抛出异常
try {
for (;;) {
E first = q.peek(); //头元素
if (first == null)
available.await(); //头元素为 null,则线程阻塞,直到插入元素将其唤醒 (1)
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll(); //头元素等待时间到期,直接返回
first = null; // don't retain ref while waiting
if (leader != null)
available.await(); //已经有其它线程在倒计时 first (2)
else {
Thread thisThread = Thread.currentThread();
leader = thisThread; //表明当前线程正在倒计时 first
try {
available.awaitNanos(delay); //等待 delay 纳秒
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
- 当队列为空时,多个线程同时执行take() 方法,所有线程会阻塞在 (1) 处。当有其它线程往队列插入元素并 condition.signal() 时,会唤醒其中一个阻塞线程。线程从 (1) 会唤醒,重新进入 for 循环,取得不为 null 的 first 元素。
- 唤醒的线程取得 first 元素后,如果 first 延迟时间已经结束,马上返回该first。否则,本线程 thisThread 会对 first 倒计时,并将 leader 设置为该线程,直到 first 延迟时间到期。
- 倒计时不会一直无阻塞的跑着 for 循环来判断 delay 是否小于 0,这样会很耗性能。所以在倒计时时,倒计时的线程会释放锁阻塞 delay 纳秒(注意不是毫秒),然后继续for 循环判断 delay 值。
- 如果在阻塞 delay 纳秒时正好有插入元素,将其它线程从 (1)出唤醒并取到不为 null 的 first,这时候 leader 派上用场了。因为读到的 leader 不为 null,它会在 (2)处继续进入到阻塞。
- 所以可以看到,对头元素进行倒计时的线程只有一个,其它线程都进行阻塞。这样不至于所有线程都在 for 循环中跑,消耗性能。
你也许会疑惑 available.awaitNanos(delay)的作用,阻塞 delay 纳秒相对于倒计时 delay 毫秒可以说是微不足道的,为什么还要阻塞 delay 纳秒呢?我个人看法是,如果不阻塞delay 纳秒,那么倒计时的线程会一直在 for 中进行无阻断的循环跑。加一个纳秒级的阻塞,会减少性能的消耗。同样,也不会因为一直保持锁导致其它线程无法插入数据。而且,这个阻塞,也不会多大影响返回 first 时超时太久。
offer
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
插入元素就比较简单了。
- 如果队列为空时插入一个元素,那么q.peek() 等于插入的元素,就得 signal() 唤醒在 take() 中阻塞的线程。
- 如果队列不为空且如果插入队列的元素正好是队列头元素,那么leader线程的等待时间就不正确了。则将 leader 设置为 null ,唤醒 take() 中阻塞的线程,并重新选择一个倒计时的线程,并将 leader 赋值新的倒计时线程。
总的来说,延迟队列的 take() 和 offer() 方法的难点主要在于线程间的通信。明白了线程安全类的 ReentrantLock 的用法,也就好理解了。
迭代器
public Iterator<E> iterator() {
return new Itr(toArray());
}
private class Itr implements Iterator<E> {
final Object[] array; // Array of all elements
int cursor; // index of next element to return
int lastRet; // index of last element, or -1 if no such
Itr(Object[] array) {
lastRet = -1;
this.array = array; //底层优先队列 p 保存数据的数组,实则是对数组的迭代
}
public boolean hasNext() {
return cursor < array.length;
}
@SuppressWarnings("unchecked")
public E next() {
if (cursor >= array.length)
throw new NoSuchElementException();
lastRet = cursor;
return (E)array[cursor++];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
removeEQ(array[lastRet]); //对底层优先队列中的元素进行删除
lastRet = -1;
}
}
toArray() 返回的是保存在优先队列的数组元素。可以看到,用迭代器遍历延迟队列时,其实就是取得保存在底层优先队列的数据的一个快照,在对这个快照进行遍历。所以说,延迟队列的迭代器是弱一致的,因为在迭代的时候,底层优先队列的数据变化了,在迭代器中保存的快照是不会实时同步变化的。而且,迭代的时候删除元素,也是先记下要删除的元素,然后对在底层优先队列中寻找该元素并删除(可能优先队列中该元素已经不存在了),所以迭代时删除元素不会抛出ConcurrentModificationException。