DelayQueue
DelayQueue是一个无界的阻塞队列,它的元素实现了Delayed接口,并且元素只有在delay到期以后才能取出。队列头是延迟过期时间最长的元素,如果没有元素过期,执行poll()方法返回null。
案例
首先定义一个实现Delayed接口的泛型类MyDelayNode,Delayed接口继承了Comparable接口。
public class MyDelayNode<T> implements Delayed {
private int delay; //过期时间
private TimeUnit unit; //单位
private long current;
private T item; //实际的数据元素
public MyDelayNode(T item, int delay, TimeUnit unit) {
this.item = item;
this.delay = delay;
this.unit = unit;
this.current = System.currentTimeMillis();
}
@Override
public long getDelay(TimeUnit unit) {
return current + this.unit.toMillis(delay) - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
return (int) this.getDelay(unit) - (int) o.getDelay(unit);
}
@Override
public String toString() {
return "MyDelayNode{" +
"item=" + item +
'}';
}
}
测试demo类
public class DelayQueueDemo {
private static final Logger logger = LoggerFactory.getLogger(DelayQueueDemo.class);
public static void main(String[] args) throws InterruptedException {
DelayQueue<MyDelayNode<String>> delayQueue = new DelayQueue<>();
for (int i = 0; i < 5; i++) { //添加5个元素到队列
delayQueue.put(new MyDelayNode<>("节点" + (i + 1), i + 1, TimeUnit.SECONDS));
}
logger.info("====================================");
while (delayQueue.peek() != null) { //循环取出元素
logger.info(delayQueue.take() + "");
}
}
}
运行结果:
10:28:44.888 [main] INFO com.example.testdemo56.DelayQueueDemo - ====================================
10:28:45.886 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点1}
10:28:46.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点2}
10:28:47.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点3}
10:28:48.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点4}
10:28:49.885 [main] INFO com.example.testdemo56.DelayQueueDemo - MyDelayNode{item=节点5}
从测试结果可以看出,5个元素间隔1s取出。
源码
先来看一下类中定义的几个字段。
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
/**
* Thread designated to wait for the element at the head of
* the queue. This variant of the Leader-Follower pattern
* (http://www.cs.wustl.edu/~schmidt/POSA/POSA2/) serves to
* minimize unnecessary timed waiting. When a thread becomes
* the leader, it waits only for the next delay to elapse, but
* other threads await indefinitely. The leader thread must
* signal some other thread before returning from take() or
* poll(...), unless some other thread becomes leader in the
* interim. Whenever the head of the queue is replaced with
* an element with an earlier expiration time, the leader
* field is invalidated by being reset to null, and some
* waiting thread, but not necessarily the current leader, is
* signalled. So waiting threads must be prepared to acquire
* and lose leadership while waiting.
*/
private Thread leader = null;
/**
* Condition signalled when a newer element becomes available
* at the head of the queue or a new thread may need to
* become leader.
*/
private final Condition available = lock.newCondition();
这几个字段都比较重要,保证线程安全的锁和阻塞线程的条件很好理解,这是阻塞队列的特点决定的。主要是这个优先队列q和线程leader,我们可以看出DelayQueue底层通过优先队列实现的,因为DelayQueue也是有顺序的,只是这个顺序是按delay时间排序的。那这个leader是做什么的呢?我们继续向下看。
因为DelayQueue也是实现的BlockingQueue接口,这里还是关注与阻塞有关的put和take方法。
先来看put方法,put方法内部调用的是offer方法,这很好理解因为DelayQueue无界,所有无界的阻塞队列都是这样实现的,因为不可能满,所以插入时不可能阻塞。
public void put(E e) {
offer(e);
}
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的offer方法插入元素,由于优先队列也是无界的,所以这里不会出现阻塞。只是插入以后判断了一下插入的元素是不是头节点,如果是执行了两行代码,这两行代码我们后面介绍。
再来看take方法。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); //同步块
try {
for (;;) { //死循环
E first = q.peek();
if (first == null) //队列为空,阻塞
available.await();
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0) //delay过期
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
单看代码,这个方法并不复杂,但是这里存在一定的逻辑,我们要理解的是这个代码为什么这么写。
我们都知道,对于阻塞队列来说,执行take方法时,如果队列为空,会阻塞,不为空,理解返回队列头元素。但是DelayQueue存在一种特殊情况,就是队列不为空,但是Delay接口的getDelay()方法返回的值大于0,这是线程同样会等待,直到Delay接口的getDelay()方法返回的值小于0才能返回队列头。
知道了这个前提,我们再来分析代码。
首先第5行是一个死循环,关注后面的代码,只有第11行满足才能终止循环,这是唯一的出口。死循环下面的3行代码,很好理解,如果队列为空,线程阻塞。这个不需要解释。如果队列不为空,同样存在两种情况。
这里我们举例来分析,假设当前队列存在5个元素,delay时间(Delay接口的getDelay()方法返回的值)分别为2,3,4,5,6,这样队列头就是delay时间为2的,并且队列头delay时间没有过期。现在存在3个线程同时调用take方法,来获取队列头。我们来分析这个过程。
- 假设线程1争抢到了lock锁,执行到第10行时,发现delay>0,肯定无法立即返回
- 判断leader是否为null,此时leader为null
- 将当前线程赋予leader,然后线程阻塞delay,释放锁
- 线程2获取锁,发现leader不为null,立即阻塞,释放锁
- 线程3获取锁,与线程2行为一样
- 线程1阻塞时间到达后,会将leader置为null,在死循环中执行到第10行,此时delay<0,退出循环,在返回队列头之前,会执行finally块中通知线程唤醒得代码
- 假设线程2被唤醒,重复执行上述1-6
这里的leader线程代表着等待队列头时间到达的线程,leader线程永远等待队列头的超时时间,除了leader线程的其他线程均无限等待直到收到唤醒通知,才有机会变成leader线程。
offer方法的这三行代码解释一下。
if (q.peek() == e) {
leader = null;
available.signal();
}
当插入的元素在队列头时,肯定会打断原有等待队列头的线程leader,然后在take方法中的死循环重新选出leader,等待队列头时候超时。
结论
DelayQueue类一个非常有用的特性是,在生产者-消费者模型中,只有在特定元素过期后才能将他们消费。另一个用途在于线程池,提交任务以后延时执行。