DelayQueue(JUC包)
这篇笔记,我们要来介绍实现Java定时任务的第三个方案,使用DelayQueue,以及该方案的优点和缺点。
DelayQueue是Java并发包java.util.concurrent中的一个无界阻塞队列,它只允许插入实现了Delay接口的对象,队列中的元素只有当其延迟时间到达之后才能被取走,我们这里就是基于DelayQueue的阻塞特性、延迟特性和无界性来实现的定时任务调度。
阻塞特性指DelayQueue的take方法会一直阻塞,直到队列中有元素的延迟时间到期,然后返回该元素。
延迟特性指每一个插入到DelayQueue的元素都有一个关联的延迟时间,只有当延迟时间到期后,该元素才能从队列中取出。
无界性指DelayQueue是无界的,可以无限添加元素,但只有延迟到期的元素才能被取出。
定时任务的大致工作流程是创建任务->任务入延迟队列->消费者线程在任务到期时取出任务->执行任务->重复上述流程。
使用
1.创建一个定时任务类
DelayQueue延迟队列只允许插入实现了Delay接口的对象,所以我们的定时任务是用一个实现了Delay接口的类来表示的。实现Delay接口必须实现两个方法,其中getDelay方法用来返回任务的剩余延迟时间,compareTo方法用来比较当前定时任务和其他定时任务的执行的先后顺序。
我们定时任务的代码逻辑通过实现Runable接口的一个类经过构造器传入到我们的定时任务类当中。
public class DelayedTask implements Delayed {
private final long executionTime;
private final Runnable task;
public DelayedTask(long delay,TimeUnit unit,Runnable task){
this.executionTime = System.currentTimeMillis() + unit.toMillis(delay);
this.task = task;
}
/**
* 返回任务的剩余延迟时间
* @param unit the time unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
long diff = executionTime - System.currentTimeMillis();
return unit.convert(diff,TimeUnit.MILLISECONDS);
}
/**
* 比较两个任务的执行时间,以便延迟队列能够正确地按顺序处理任务
* @param o the object to be compared.
* @return
*/
@Override
public int compareTo(Delayed o) {
return Long.compare(this.executionTime,((DelayedTask) o).executionTime);
}
public void run(){
task.run();
}
}
2.创建一个定时任务消费者类
接着,我们需要创建一个定时任务消费者类,这个类的实例将从延迟队列中取出到达指定时间的定时任务,执行相应的代码逻辑。DelayQueue的take方法会阻塞地获取任务,所以我们最好实现一个Runnable接口,通过开辟主线程之外的线程来消费定时任务。
public class TaskConsumer implements Runnable{
private final DelayQueue<DelayedTask> queue;
public TaskConsumer(DelayQueue<DelayedTask> queue){
this.queue = queue;
}
@Override
public void run() {
while(true){
try {
//阻塞地获取任务,从延迟队列中取出任务并执行
DelayedTask task = queue.take();
task.run();
} catch (InterruptedException e) {
//中断异常,结束线程
Thread.currentThread().interrupt();
break;
}
}
}
}
3.创建一个定时任务生产者类
然后,我们还需要一个将定时任务添加到延迟队列中的生产者,这里我们也在主线程之外进行添加,这一步可以根据具体的需要进行删除或保留,在实际需求中,我们可能在主线程中将定时任务添加到延迟队列即可。
public class TaskProducer implements Runnable{
private final DelayQueue<DelayedTask> queue;
public TaskProducer(DelayQueue<DelayedTask> queue) {
this.queue = queue;
}
@Override
public void run() {
//添加一些任务
queue.put(new DelayedTask(5, TimeUnit.SECONDS,() -> System.out.println(getTime()+"任务1执行")));
queue.put(new DelayedTask(10, TimeUnit.SECONDS,() -> System.out.println(getTime()+"任务2执行")));
queue.put(new DelayedTask(15, TimeUnit.SECONDS,() -> System.out.println(getTime()+"任务3执行")));
}
/**
* 获取当前系统时间
* @return
*/
public static String getTime(){
//获取当前的系统时间
LocalDateTime now = LocalDateTime.now();
//定义时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
//格式化时间
return now.format(formatter);
}
}
4.启动生产者和消费者进程,实现定时任务
最后,同时启动生产者和消费者进程,就可以添加和执行定时任务了。
public class DelayQueueExample {
public static void main(String[] args) {
DelayQueue<DelayedTask> queue = new DelayQueue<>();
Thread consumerThread = new Thread(new TaskConsumer(queue));
Thread producerThread = new Thread(new TaskProducer(queue));
consumerThread.start();
producerThread.start();
}
}
优点
1.简单易用
DelayQueue是Java自带的并发工具类,不需要额外引入依赖。通过简单的生产者-消费者模式即可实现定时任务调度。
2.高精度延迟控制
DelayQueue提供了精确的延迟控制功能,只有当任务的延迟时间到达后,任务才会从队列中取出并执行,适用于需要高精度延迟的任务场景。
3.线程安全
DelayQueue是线程安全的,多个线程可以同时对其进行操作而不会出现数据不一致的问题,适合多线程环境下的任务调度。
4.阻塞特性
DelayQueue的take方法会阻塞到有任务到期为止,减少了不必要的轮询和资源浪费,提高了系统的效率。
5.灵活性
可以根据实际需要自定义Delayed接口的实现类,灵活控制任务的延迟时间和执行逻辑。
6.使用于一次性任务
对于只需要执行一次的任务,DelayQueue是个不错的选择,它能够高效地管理这些一次性的任务,并在指定的时间点触发执行。
缺点
1.不支持周期性任务
DelayQueue只能处理一次性延迟任务,无法直接支持周期性任务。如果需要周期性执行的任务,必须手动重新提交任务到队列中,增加了复杂性和出错的可能性。
2.依赖外部线程管理
需要自己管理生产者和消费之线程,这不仅增加了代码的复杂性,还可能导致资源管理不当,出现线程泄漏或线程阻塞问题。
3.性能开销较大
每次从DelayQueue中取出元素时,都会进行一次排序操作以确保延迟最小的元素优先出队,对于高并发或大量任务场景,这种排序操作可能会带来较大的性能开销。
4.缺乏高级调度功能
DelayQueue提供的功能相对简单,缺少一些高级调度特性,如任务优先级、任务依赖关系、任务重试机制等,这些高级调度特性在复杂的业务场景中往往是必需的。
5.异常处理复杂
如果任务执行过程中发生异常,DelayQueue本身是不会自动处理这些异常的,需要我们自己在消费者线程中添加额外的异常处理逻辑,以确保系统的稳定性和可靠性。
6.不适合长时间运行任务
如果任务的执行时间超过了其设定的延迟时间,可能会导致后续任务的延迟执行,进而影响整个调度系统的准确性。