延迟阻塞队列-DelayQueue

原文链接 https://www.treeroot.tech/archives/DelayQueue

DelayQueue

概述

我们一般业务需求会有延迟任务的需求,简单的可以使用Timer, 但是Timer支持单线程,如果使用希望多线程执行任务的话可以使用ScheduledThreadPoolExecutor,这两个还都属于内存版的消息队列,如果希望消息持久化保存,则可以选择quartz(可以配置数据库保存消息),以及MQ等,今天先说说Timer和ScheduledThreadPoolExecutor的实现,这两个的实现内部使用了类似DelayQueue的队列,所以先看看DelayQueue的原理。

基本介绍

其实DelayQueue是一个BlockingQueue和PriorityQueue的结合,也就是阻塞队列和优先级队列, 阻塞队列是可以实现获取队列元素时,如果队列为空会阻塞当前读取的线程而优先级队列其实就是对于加入的队列的元素会进行排序(使用堆排序),所以加入优先级队列的元素必须实现Comparable接口,如果队列是有序的那就好办了,当先写入一个 100秒执行的任务最后在写入一个10秒的任务,那么10秒任务会排在100秒任务之前,这有什么用呢,先让我们看看这个Demo,下面会解释。

public class Main {
    public static void main(String[] args) {
        DelayQueue<DelayTask> delayQueue = new DelayQueue<>();
        delayQueue.offer(new DelayTask(20000));
        delayQueue.offer(new DelayTask(10000) );
        new Thread(() -> {
            while (true) {
                try {
                    DelayTask task = delayQueue.take();
                    System.out.println("有任务来了");
                    task.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private static class DelayTask implements Delayed, Runnable {

        private long delayTime;
        private String name;

        /**
         * 默认单位毫秒
         */
        public DelayTask(long delayTime) {
            this.delayTime = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delayTime);
            this.name = String.valueOf(delayTime);
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.toNanos(delayTime) - System.nanoTime();
        }

        @Override
        public int compareTo(Delayed o) {
            if (getDelay(TimeUnit.NANOSECONDS) < o.getDelay(TimeUnit.NANOSECONDS)) {
                return -1;
            } else if (getDelay(TimeUnit.NANOSECONDS) > o.getDelay(TimeUnit.NANOSECONDS)) {
                return 1;
            } else {
                return 0;
            }
        }

        @Override
        public void run() {
            System.out.println(name + "ms 的任务执行了");
        }
    }
}

从代码上可以看到可以起一个线程死循环从DelayQueue里获取消息,先看看DelayQueue.take()方法的实现。

/**
     * Retrieves and removes the head of this queue, waiting if necessary
     * until an element with an expired delay is available on this queue.
     *
     * @return the head of this queue
     * @throws InterruptedException {@inheritDoc}
     */
    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();
        }
    }

首先从queue里peek一个元素(也就是我们的延迟任务),peek方法是获取元素但是不移除元素,

  • first等于null则直接一直await下去,可以如果一直await当前线程不是被永远阻塞住了吗,其实不会,因为阻塞队列就是一个生产者消费者模型的实现,如果有其他线程往队列里添加新元素则这里得await就会返回。
  • first不等于null,则判断first的剩余到期时间,如果剩余时间小于0则调用poll(获取并移除元素)方法取出元素返回,如果剩余到期时间大于0则awaitNanos阻塞在这并且设置超时时间为first的剩余到期时间,如果这里设置为first的剩余到期时间就阻塞在这了那其他的任务时间到期怎么办,这就是优先级队列排序的妙处,排在第一个的永远是最近要被执行的任务,所以这里即使阻塞了,其他的任务也一定晚于这个任务被执行,如果这时有新的比first还要更近的时间任务被添加进了队列, 那么awaitNanos就会返回也不会耽误这个最新任务的执行。

Timer和ScheduledThreadPoolExecutor

能明白了DelayQueue的原理,那么Timer和ScheduledThreadPoolExecutor就不难理解了,Timer内部的TimerQueue和ScheduledThreadPoolExecutor内部的DelayedWorkQueue原理跟DelayQueue是一样的。Timer执行任务只支持单线程,ScheduledThreadPoolExecutor支持多线程执行,Timer是一个线程一个延迟队列,而ScheduledThreadPoolExecutor是多个线程一个延迟队列。
下一篇介绍延迟任务时间轮的实现。
Golang 实现 DelayQueue 参考 Java github地址
Kafka 多级时间轮的 Java 实现 github地址

你可能感兴趣的:(算法,Java语言,数据结构)