延迟队列DelayQueue源码解析

335 阅读2分钟
前置知识:
  1. 底层数据存储结构为PriorityQueue没有指定大小所以为无界队列
private final PriorityQueue<E> q = new PriorityQueue<E>();
  1. 队列元素需要实现继承了Comparable接口的Delayed接口,接口中需要实现的方法有两个getDelay和compareTo
public interface Delayed extends Comparable<Delayed> {
    //获取当前的延迟时间
    long getDelay(TimeUnit unit);
}
public interface Comparable<T> {
    //比较方法
    public int compareTo(T o);
}
  1. 并发模式为领导者/追随者模式
使用测试
@Test
public void test() throws InterruptedException {
    //创建一个延后5s执行的node
    TestNode testNode = new TestNode(System.currentTimeMillis() + 5000L, new Runnable() {
        @Override
        public void run() {
            System.out.println("执行了延迟任务");
        }
    });

    DelayQueue<TestNode> queue = new DelayQueue<>();
    queue.offer(testNode);

    while(!queue.isEmpty()){
        //take()为阻塞获取
        TestNode take = queue.take();
        //因为DelayQueue的并发模式为领导者/追随者模式,所以可以直接用当前线程来执行task并不会阻塞下一个任务的获取
        take.getTask().run();
    }
    System.out.println("test结束");
}

//参考了ScheduledThreadPoolExecutor的实现
public class TestNode implements Delayed {
    //需要执行的时间
    private long time;
    //需要执行的任务
    private Runnable task;

    public TestNode(long time,Runnable task) {
        this.time = time;
        this.task = task;
    }

    public long getTime() {
        return time;
    }

    public Runnable getTask() {
        return task;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(time - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        //延迟时间按从小到大的规则比较
        long diff = getDelay(NANOSECONDS) - o.getDelay(NANOSECONDS);
        return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
    }
}
offer()方法源码
public boolean offer(E e) {
    //加锁保证线程安全
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //将元素加入优先队列
        q.offer(e);
        //如果当前加入的元素为队列最值,将leader线程置为null(leader应为为消费线程)
        //同时唤醒正在等待消费的阻塞线程
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}
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)
                    return q.poll();
                first = null; 
                if (leader != null)
                    //当前leader线程不为null说明已经有其它线程在等待消费,阻塞当前线程
                    available.await();
                else {
                    //当前leader线程为null,则将当前线程设置为leader线程,阻塞delay的时间后进行重试消费
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        available.awaitNanos(delay);
                    } finally {
                        //当前线程在下次循环中会获取队首元素直接返回,所以需要将leader线程置为null
                        //表示目前队列中没有线程在等待消费
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            //leader线程为null说明没有线程在等待消费延迟任务,需要尝试唤醒在等待实时消费的线程
            available.signal();
        lock.unlock();
    }
}

总结

DelayQueue基于优先级队列 + Leader-Follower的线程等待模式,来实现了只允许取出到期队列元素的功能。获取队首元素的线程由于队首元素是整个队列中最先延迟结束的元素,所以线程只等待特定的延迟时间后即可进行获取消费,同时在这期间当前线程就是leader,其它后来的消费线程则作为follower阻塞等待,直到leader取出要元素进行消费时随机唤醒一个follower使其拿到变成新的leader,如果新入队了一个剩余延迟时间更短的元素导致leader失去领导权也会随机唤醒一个线程成为新的leader,如此往复。

ScheduledThreadPoolExecutor中定时任务的核心逻辑与DelayQueue相似,不过ScheduledThreadPoolExecutor使用了自己定义的内部类DelayedWorkQueue将DelayQueue的接口方法改为了内部实现,使其可以更加灵活的添加定时任务独有的方法调用。