1.结构
DelayQueue 继承关系,核心成员变量及主要构造函数:
// 队列中的元素都要实现Delayed接口,实现后每个实体对象就有过期时间了
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E>{
// 组合PriorityQueue进行队列操作
private final PriorityQueue<E> q = new PriorityQueue<E>();
// reentrantLock保证线程安全
private final transient ReentrantLock lock = new ReentrantLock();
// 出队休眠Condition。其实很容易理解:当有线程来取元素,但队列中为空或者没有元素过期,所以要进入条件队列等待
private final Condition available = lock.newCondition();
// 条件队列中剩余等待时间最短的线程,一般是先来的线程
// 注:虽然引入了leader,但是也并非公平的(1.非公平锁 2.当新线程能获取元素时就直接返回了)
private Thread leader = null;
//--------------------------------构造函数-------------------------------
public DelayQueue() {
}
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
}
- PriorityQueue 中文叫做优先级队列,在此处的作用就是可以根据过期时间做优先级排序,让先过期的可以先执行。具体请参考【Java容器源码】PriorityQueue源码分析。
- 复用一直是个常见且重要的话题,就比如此处 DelayQueue 复用 PriorityQueue 的能力, 还有 LinkedHashMap 复用 HashMap 的能力,Set 复用 Map 的能力。小结一下,如果想要复用需要做到哪些:
- 需要把能遇见可复用的功能尽量抽象,并开放出可扩展的地方,比如说 HashMap 在操作数组的方法中,都给 LinkedHashMap 开放出很多 after 开头的方法,便于 LinkedHashMap 进行排序、删除等等;
- 采用组合或继承两种手段进行复用,比如 LinkedHashMap 采用的继承、 Set 和 DelayQueue 采用的组合,组合的意思就是把可复用的类给依赖进来。
<E extends Delayed>
:即队列中的所有元素都应该实现 Delayed 接口。从下面的源码可以看出,Delayed 还继承了 Comparable 接口,所以队列中的元素应该实现以下两个方法:- getDelay:获取过期时间。所以这里一定要清楚,每个元素的过期时间是在放入队列前就确定的,而不是在放入队列时作为参数传进来。
- compareTo:用 getDelay 的过期时间对所有元素进行排序
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
2.方法解析 & api
2.1 入队
put()
public void put(E e) {
// 调用offer
offer(e);
}
offer()
offer 的逻辑其实很简单
-
调用 PriorityQueue 的 offer 方法将新元素添加进来。因为 PriorityQueue 的底层是最小堆,所以会对新元素进行上浮
-
若这个新元素刚好成了队首(堆顶),那么就要立刻唤醒一个等待的线程,然后删除现在的leader,让新线程有机会成为 leader。
这么做的原因是leader必须是条件队列中剩余等待时间最短的线程(等待时间=队首过期时间),但现在来了一个过期时间更短的新队首,所以应该有一个更短的线程来当leader。对这句话不理解的话,对比下面的take方法就一目了然了。
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); // 上锁
try {
// 直接调用 PriorityQueue 的 offer 方法,从而使用它的排序和扩容等能力
q.offer(e);
// 如果恰好刚放进去的元素正好在队列头
// 立马唤醒一个在条件队列等待线程,并将现在leader置为null,让那个新唤醒的线程有机会成为leader
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock(); // 释放锁
}
}
2.2 出队
take()
take 方法的过程如下:
-
拿到锁的线程可以进入自旋
-
取出队首 first,分为以下两种情况:
-
情况 1:first=null,队列空,休眠当前线程
-
情况 2:first != null,即队列中有元素,那么又要看该元素是否过期
-
情况 2.1:过期时间delay<0,即队头已经过期,可以取出,返回
-
情况 2.2 :队头没过期,那么就要将当前线程放入条件队列休眠等待,但这时又要考虑当前线程能否当leader
-
情况 2.2.1:若当前线程是第一个来的线程就将其存入leader,然后定时休眠
注:这并不能保证公平,更不能保证自动醒来后一定得到队首 -
情况 2.2.2:已经有leader了,则当前线程直接休眠
-
-
-
-
释放锁
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly(); // 加锁,可中断
try {
// 自旋 保证一定能成功
for (;;) {
// 拿出队首first
E first = q.peek();
// 情况 1:first=null,队列为空,休眠当前线程
if (first == null)
available.await();
// 情况 2:队列非空
else {
long delay = first.getDelay(NANOSECONDS); // 获取队首元素(first)的过期时间delay
// 情况 2.1:first已经过期,则可以出队
if (delay <= 0)
return q.poll();
first = null; // 将first引用置为 null ,便于gc
// 情况 2.2:first未过期
// 情况 2.2.1:leader!=null,即在当前线程之前已经有线程等待取出了,所以直接休眠当前线程
if (leader != null)
available.await();
// 情况 2.2.2:leader == null,即当前线程当前线程是第一个要来取出的线程,存入leader
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 给leader计时休眠,释放锁
// 到时自动醒来,大概率取得队首
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock(); // 释放锁
}
}
poll()
相较于 take,poll 会在队列为空或者队首未过期时直接返回 null,而不是阻塞在那里
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
// !!!如果队列为空,或队首没过期,直接放回null
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
另外,如果不想一直无限阻塞,可以调用poll设置阻塞时间
2.3 获取队首:peek
public E peek() {
// 虽然只是查看队首,但也要获取锁,防止在查看时被修改
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 调用priortyQueue的peek
return q.peek();
} finally {
lock.unlock();
}
}
3.使用实例
public class DelayQueueDemo {
@Data
// 队列元素,实现了 Delayed 接口
static class DelayedDTO implements Delayed {
Long s;
Long beginTime;
public DelayedDTO(Long s,Long beginTime) {
this.s = s;
this.beginTime =beginTime;
}
// 重写getDelay,获取过期时间
public long getDelay(TimeUnit unit) {
return unit.convert(s - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
// compareTo
public int compareTo(Delayed o) {
return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
}
// 生产者
static class Product implements Runnable {
// DelayQueue是BlockQueue的实现类,因此这里使用接口更灵活
private final BlockingQueue queue;
public Product(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
log.info("begin put");
long beginTime = System.currentTimeMillis();
// 放入队列,延迟 2 秒执行
queue.put(new DelayedDTO(System.currentTimeMillis() + 2000L,beginTime));
// 延迟 5 秒执行
queue.put(new DelayedDTO(System.currentTimeMillis() + 5000L,beginTime));
// 延迟10秒执行
queue.put(new DelayedDTO(System.currentTimeMillis() + 1000L * 10,beginTime));
log.info("end put");
} catch (InterruptedException e) {
log.error("" + e);
}
}
}
// 消费者
static class Consumer implements Runnable {
private final BlockingQueue queue;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
log.info("Consumer begin");
// 从队列中取
((DelayedDTO) queue.take()).run();
((DelayedDTO) queue.take()).run();
((DelayedDTO) queue.take()).run();
log.info("Consumer end");
} catch (InterruptedException e) {
log.error("" + e);
}
}
}
// Mian
public static void main(String[] args) throws InterruptedException {
BlockingQueue q = new DelayQueue();
// 将delayQueue传入生产者与消费者
DelayQueueDemo.Product p = new DelayQueueDemo.Product(q);
DelayQueueDemo.Consumer c = new DelayQueueDemo.Consumer(q);
new Thread(c).start();
new Thread(p).start();
}
}
执行结果如下:
06:57:50.544 [Thread-0] Consumer begin
06:57:50.544 [Thread-1] begin put
06:57:50.551 [Thread-1] end put
06:57:52.554 [Thread-0] 延迟了2秒钟才执行
06:57:55.555 [Thread-0] 延迟了5秒钟才执行
06:58:00.555 [Thread-0] 延迟了10秒钟才执行
06:58:00.556 [Thread-0] Consumer end