DelayQueue
是一种特殊的阻塞队列,只有到期的对象,才能从队列中取出。
底层有用到 PriorityQueue
,入队时会进行排序。也就是说,这个阻塞队列是有序的。
典型的应用场景,比如:12306订票,30分钟内未支付,则取消订单。
实现这样的功能,用定时任务是刷,当然可以。但用DelayQueue
会更精确。
public static void main(String[] args) throws Exception {
long base = System.currentTimeMillis();
DelayQueue<Food> queue = new DelayQueue<>();
for(int i = 0; i < 10; i++){
String name = "food_" + i;
int cookedMinutes = RandomUtil.randomInt(1,20);
Food food = new Food(name, cookedMinutes);
log.info("name:{}, cookeMinutes:{}",name, cookedMinutes);
queue.offer(food);
}
log.info("all foods OK");
for(int i = 0; i < 10; i++){
Food food = queue.take();
log.info("foodName:{}, time:{}",food.name,(food.cookedSeconds - base) / 1000 );
}
static class Food implements Delayed{
private String name;
private long cookedSeconds;
Food(String name, int cookedSeconds){
this.name = name;
this.cookedSeconds = System.currentTimeMillis() + cookedSeconds * 1000;
}
@Override
public long getDelay(TimeUnit unit) {
return cookedSeconds - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
long result = this.getDelay(TimeUnit.SECONDS) - o.getDelay(TimeUnit.SECONDS);
return result == 0 ? 0 : result > 0 ? 1 : -1;
}
}
这个示例代码,你可以理解为涮火锅。
各种食材,煮熟所需要的时间是不同的。
吃的时候,只捞煮熟的。
只有实现了Delayed
的接口,这样的元素才能放入 DelayQueue
。
在示例代码中重写了两个方法 getDelay()
。只有这个方法返回值 小于等于 0,才会出队。
重写 compareTo
方法,是给 PriorityQueue
排序用。
你可以运行下示例代码,看下效果。可以实现,类似吃火锅时,只捞起煮熟的菜。
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();
讲入队源码之前,先看下 DelayQueue
中的几个成员变量。
ReentrantLock lock
是个全局锁,相关操作的时候,需要加锁。
PriorityQueue
,这是个优先级队列,存放元素的。之前在介绍 PriorityBlockingQueue 时,详细说过堆化、排序等问题。
Thread leader
,这个很有意思,英文好的,看下源码的注释,这个是用来控制出队竞争的。
Condition available
,这个是用来阻塞与唤醒的。详细的分析,可以看我以前的博文:《ArrayBlockingQueue 源码解析》
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();
}
}
从入队源码来看,入队操作,不会阻塞。在加锁的情况下,元素被放入 PriorityQueue
中。
而 PriorityQueue
是会自动扩容的,自动排序的。
q.peek() == e
进行这个判断是做什么的?
还是拿刚刚吃火锅的例子:
你先在锅里放了些土豆,一时半会儿熟不了。
这时你又往里放了盘青菜。青菜虽然比土豆放的晚,可它会先熟。
你得吆喝两声,来,来,来,捞青菜吃。
q.peek() == e
这个的意思是所放元素,会第一个出队。 相当于青菜。
leader = null
和 available.signal()
,是通知出队线程,取元素。相当于那两声吆喝。
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)
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();
}
}
出队的逻辑,可以用刚刚吃火锅的例子,形象的讲一下。
首先,夹菜必需用公筷,公筷只有一双哈。
其中一个人,拿着公筷夹起一片肥牛,看成色不熟,就开始涮几下。
这时,其它几个人,要是也想夹菜,那得排队等着,因为公筷就那一双。
那人涮了几个后,又夹起来,看那肉熟了。肉夹到自己碗里,放下公筷。
这时,他吆喝一声,下一个。那别人就用这个公筷夹菜了。
这里的公筷,就相当于代码中的 leader,
leader 为空,可以取元素。
leader不为空,说明有线程在等着出队,其它线程调用 await()
方法排队等待。
await()
方法,先进入等待队列,再解锁、之后阻塞、被唤醒后抢锁。
signal()
方法,是从等待队列中取一个节点,放入CLH 队列,之后有机会去抢锁。
这两个方法的源码解析,看我之前的这篇博文《ArrayBlockingQueue 源码解析》
有上面的铺垫,咱们再开始细说,出队源码。
假设有 t1, t2, t3, t4 这四个线程,同时来执行take()
方法。
首先,是 lock.lockInterruptibly();
这一行是抢锁,假设 t1 抢到了锁。
t2, t3, t4 阻塞在这一行。效果如图。(AQS 框架不熟悉的话,看下这篇博文《ReentrantLock源码解析》)
假设 first == null
,那 t1 线程,会执行 available.await()
,它会入等待队列,释放锁,阻塞。
在 t1 进入等待队列后,释放锁之前,会是下面这个样子。
当 t1 释放了锁,那么t2, t3, t4 去抢锁,某个线程抢到了锁,和 t1 线程一个命运。
会执行 available.await()
入队,释放锁,阻塞。 如此往复,最终四个线程都会阻塞到这行代码。
假如说 t5 线程,现在执行了 offer(E e)
方法,队列中有元素了,那会调用 available.signal()
这行。
那么会从等待队列中取出一个节点,放到CLH队列中抢锁。
t5 线程 释放锁的时候,会唤醒 CLH 队列中 最前面的节点。
那原来 t1 节点阻塞在 available.await();
唤醒后,拿到锁,会跳出这行代码。
比如在执行 long delay = first.getDelay(NANOSECONDS);
这行时,delay 大于 0 。
相当于肥牛还没熟,那就继续往下执行,leader = thisThread;
相当于拿到公筷啦。
执行 available.awaitNanos(delay)
; 比如还有5秒到时间,那它就等待5秒。
available.awaitNanos(delay)
方法和 available.await()
类似,
进入等待队列,解锁,限时阻塞,自我唤醒
限时阻塞,指阻塞一定时间后,自动唤醒自己,进入 CLH 队列,抢到锁,跳出 awaitNanos()
方法。
若是在 t1 限时阻塞期间,有别的线程来执行 take()
方法, 执行 leader != null
时,会返回true,
因为公筷是t1 拿着,其它线程即使来了,也会阻塞在 available.await();
这行代码中。
当 t1 线程限时阻塞结束、自我唤醒拿到锁,就跳出 awaitNanos
此时阻塞队列中,是有可以取走的元素,之前是肥牛没熟,现在熟了……
t1 丢掉了公筷,第二次进入 for 循环,会取走上次没有取走的肥牛。
在 return 之前,执行 finally中的代码
if (leader == null && q.peek() != null) // 锅里有肉,公筷没有被占用
available.signal(); // 唤醒一个节点,让它去抢锁
lock.unlock(); // 释放锁
至此为此,take()
方法讲完了,这里面哪里阻塞,怎么被唤醒的,都说了下。
代码逻辑不复杂,这里面并发控制的真好,阻塞也设计的很好,尤其是公筷的设计,特别棒。
Doug Lea 真的是大牛,写的代码太牛了!
至此,阻塞队列中,常用的五个阻塞队列,源码逐一做了分析。
包括,
对于不同的场景,选择不同的阻塞队列,我也写了一篇总括的文章。《浅谈五种常用的阻塞队列》。