Java并发编程入门(二十五)DelayQueue

448 阅读4分钟

banner窄.png

铿然架构  |  作者  /  铿然一叶 这是铿然架构的第 90 篇原创文章

相关阅读:

Java并发编程(一)知识地图
Java并发编程(二)原子性
Java并发编程(三)可见性
Java并发编程(四)有序性
Java并发编程(五)创建线程方式概览
Java并发编程入门(六)synchronized用法
Java并发编程入门(七)轻松理解wait和notify以及使用场景
Java并发编程入门(八)线程生命周期
Java并发编程入门(九)死锁和死锁定位
Java并发编程入门(十)锁优化
Java并发编程入门(十一)限流场景和Spring限流器实现
Java并发编程入门(十二)生产者和消费者模式-代码模板
Java并发编程入门(十三)读写锁和缓存模板
Java并发编程入门(十四)CountDownLatch应用场景
Java并发编程入门(十五)CyclicBarrier应用场景
Java并发编程入门(十六)秒懂线程池差别
Java并发编程入门(十七)一图掌握线程常用类和接口
Java并发编程入门(十九)异步任务调度工具CompleteFeature
Java并发编程入门(二十)常见加锁场景和加锁工具
Java并发编程入门(二十一)volatile关键字
Java并发编程入门(二十二)ThreadLocal变量
Java并发编程入门(二十三)守护线程
Java并发编程入门(二十四)Java原子类


1. DelayQueue介绍

先看下类结构:

delay_01.jpg

1.放到DelayQueue的对象必须实现Delayed接口

2.通过实现Delayed接口的getDelay方法返回延迟的时间,放入DelayQueue的对象在超过这个时间后才可以从队列中取出,实现延迟的效果。

3.通过实现Comparable接口进行排序,决定从队列中先取出哪个对象,通常就是按照getDelay方法的结果排序,这样延迟最短的可以先取出来。

2. 应用场景

对于DelayQueue的应用场景,要先理解延迟和失效在特定场景下可能是两个不同的概念,延迟就是延迟多少时间后执行,而失效不一定等于延迟,例如缓存未使用的失效时间是2分钟,超过2分钟就清掉缓存,但在失效前缓存被使用了,这个失效时间就会被延长。看下图:

delay_02.jpg

DelayQueue里的对象一旦放入,就不会重新排序,除非删除该元素,重新放入,而DelayQueue是个阻塞的优先级队列,队首的元素不被取出,后面的元素也不会被取出,如果队尾的元素延迟时间变化,减少了,但是没有重排序,那么也会因为队首元素未取出而取不出来,因此DelayQueue并不适合元素延迟时间会动态变化的场景,例如缓存,闲置的数据库连接池等等,它们的失效时间都会因为被使用而延长。

当然,如果要通过删除失效时间已经变化的元素,再重新放入DelayQueue队列来重排序也不是不可以,但一般不会这么做,因为对于频繁使用缓存元素的场景,每次使用到缓存时都对队列做一次重排序这个代价太大;因此对于缓存和闲置资源池的清理,通常都是通过一个定时执行的线程来遍历所有元素判断是否超过失效时间来清理。

3. DelayQueue例子

实现任务延迟执行。

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class DelayQueueDemo {

    // 定义一个可延迟执行的任务
    private static class Task implements Delayed {
        private String name;
        private long start;
        private long delayMillis;

        Task(String name, long delayMillis) {
            this.name = name;
            this.delayMillis = delayMillis;
            start = System.currentTimeMillis();
        }

        @Override
        public long getDelay(TimeUnit unit) {
            long result = unit.convert((start + delayMillis) - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
            return result;
        }

        @Override
        public int compareTo(Delayed o) {
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        }

        public void exec() {
            System.out.println(name + " was called.");
        }
    }

    private static class TaskTimer implements Runnable {
        private static volatile TaskTimer instance;
        DelayQueue<Task> delayQueue  = new DelayQueue();

        private TaskTimer() {}

        public static TaskTimer getInstance() {
            if (instance == null) {
                synchronized (TaskTimer.class) {
                    if (instance == null) {
                        instance = new TaskTimer();
                    }
                }
            }
            return instance;
        }

        public void add(Task task) {
            delayQueue.put(task);
        }

        @Override
        public void run() {
            while (true) {
                try {
                    Task task = delayQueue.take();  // 阻塞队列,从队首开始取任务
                    task.exec();
                } catch(InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        TaskTimer taskTimer = TaskTimer.getInstance();
        new Thread(taskTimer).start();

        addTask("task1", 3000);
        addTask("task2", 1500);
    }

    private static void addTask(String taskName, long delayMillis) {
        Task task = new Task(taskName, delayMillis);
        TaskTimer.getInstance().add(task);
    }
}

打印日志:

task2 was called.
task1 was called.

task2在1.5秒后执行,task1在3秒后执行。

4. 小结

1.DelayQueue是一个阻塞的优先级队列。

2.DelayQueue队列元素在入队时排序,入队后即使元素的getDelay方法返回值变化,也不会重排序,除非重新入队,因此不太适合延迟时间会动态变化的场景,例如缓存释放,闲置连接池释放。

3.可以考虑通过DelayQueue实现限流场景,当请求太多处理不过来时,可以将部分请求放到延迟队列中,过一段时间后取出来执行。

4.开源定时器也能实现任务延迟执行的效果,相比之下DelayQueue更加轻量,可根据实际情况使用。


<--阅过留痕,左边点赞!