Java 定时任务实现原理详解 您所在的位置:网站首页 闹钟定时原理是什么 Java 定时任务实现原理详解

Java 定时任务实现原理详解

2024-06-03 03:55| 来源: 网络整理| 查看: 265

在jdk自带的库中,有两种技术可以实现定时任务。一种是使用Timer,另外一个则是ScheduledThreadPoolExecutor。下面为大家分析一下这两个技术的底层实现原理以及各自的优缺点。

一、Timer 1. Timer的使用 class MyTask extends TimerTask{ @Override public void run() { System.out.println("hello world"); } } public class TimerDemo { public static void main(String[] args) { //创建定时器对象 Timer t=new Timer(); //在3秒后执行MyTask类中的run方法,后面每10秒跑一次 t.schedule(new MyTask(), 3000,10000); } }

通过往Timer提交一个TimerTask的任务,同时指定多久后开始执行以及执行周期,我们可以开启一个定时任务。

2. 源码解析

首先我们先来看一下Timer这个类

//存放定时任务的队列 //这个TaskQueue 也是Timer内部自定义的一个队列,这个队列通过最小堆来维护队列 //下一次执行时间距离现在最小的会被放在堆顶,到时执行线程直接获取堆顶任务并判断是否执行即可 private final TaskQueue queue = new TaskQueue(); //负责执行定时任务的线程 private final TimerThread thread = new TimerThread(queue); public Timer() { this("Timer-" + serialNumber()); } public Timer(String name) { //设置线程的名字,并且启动这个线程 thread.setName(name); thread.start(); }

再来看一下TimerThread 这个类,这个类也是定义在Timer.class中的一个类,它继承了Thread类,所以可以直接拿来当线程使用。 我们直接来看他的构造方法以及run方法

//在Timer中初始化的时候会将Timer的Queue赋值进来 TimerThread(TaskQueue queue) { this.queue = queue; } public void run() { try { //进入自旋,开始不断的从任务队列中获取定时任务来执行 mainLoop(); } finally { // Someone killed this Thread, behave as if Timer cancelled synchronized(queue) { newTasksMayBeScheduled = false; queue.clear(); // Eliminate obsolete references } } } private void mainLoop() { while (true) { try { TimerTask task; boolean taskFired; //加同步 synchronized(queue) { //如果任务队列为空,并且newTasksMayBeScheduled为true,就休眠等待,直到有任务进来就会唤醒这个线程 //如果有人调用timer的cancel方法,newTasksMayBeScheduled会变成false while (queue.isEmpty() && newTasksMayBeScheduled) queue.wait(); if (queue.isEmpty()) break; // 获取当前时间和下次任务执行时间 long currentTime, executionTime; //获取队列中最早要执行的任务 task = queue.getMin(); synchronized(task.lock) { //如果这个任务已经被结束了,就从队列中移除 if (task.state == TimerTask.CANCELLED) { queue.removeMin(); continue; // No action required, poll queue again } //获取当前时间和下次任务执行时间 currentTime = System.currentTimeMillis(); executionTime = task.nextExecutionTime; //判断任务执行时间是否小于当前时间,表示小于,就说明可以执行了 if (taskFired = (executionTime 1)) period >>= 1; //加锁同步 synchronized(queue) { if (!thread.newTasksMayBeScheduled) throw new IllegalStateException("Timer already cancelled."); //设置任务的各个属性 synchronized(task.lock) { if (task.state != TimerTask.VIRGIN) throw new IllegalStateException( "Task already scheduled or cancelled"); task.nextExecutionTime = time; task.period = period; task.state = TimerTask.SCHEDULED; } //将任务加入到队列中 queue.add(task); //如果任务加入队列后排在堆顶,说明该任务可能马上可以执行了,那就唤醒执行线程 if (queue.getMin() == task) queue.notify(); } } 3. 总结

Timer的原理比较简单,当我们初始化Timer的时候,timer内部会启动一个线程,并且初始化一个优先级队列,该优先级队列使用了最小堆的技术来将最早执行时间的任务放在堆顶。 当我们调用schedule方法的时候,其实就是生成一个任务然后插入到该优先级队列中。最后,timer内部的线程会从优先级队列的堆顶获取任务,获取到任务后,先判断执行时间是否到了,如果到了先设置下一次的执行时间并调整堆,然后执行任务。如果没到执行时间那线程就休眠一段时间。 关于计算下次任务执行时间的策略: 这里设置下一次执行时间的算法会根据传入peroid的值来判断使用哪种策略: - 如果peroid是负数,那下一次的执行时间就是当前时间+peroid的值 - 如果peroid是正数,那下一次执行时间就是该任务这次的执行时间+peroid的值。 这两个策略的不同点在于,如果计算下次执行时间是以当前时间为基数,那它就不是以固定频率来执行任务的。因为Timer是单线程执行任务的,如果A任务执行周期是10秒,但是有个B任务执行了20几秒,那么下一次A任务的执行时间就要等B执行完后轮到自己时,再过10秒才会执行下一次。 如果策略是这次任务的执行时间+peroid的值就是按固定频率不断执行任务了。读者可以自行模拟一下

二、ScheduledThreadPoolExecutor 1. 使用 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(8); scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("hello world"); } }, 1, 3, TimeUnit.SECONDS); 2. 实现原理+源码解析

由于ScheduledThreadPoolExecutor是基于线程池实现的。所以了解它的原理之前读者有必要先了解一下Java线程池的实现。关于Java线程池的实现原理,可以看我的另外一篇博客:Java线程池实现原理详解

我们直接来看一下的源码

public ScheduledFuture scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (period task) { //先判断是否可以在当前状态下执行 if (canRunInCurrentRunState(true)) { //重新加任务放到任务队列中 super.getQueue().add(task); if (!canRunInCurrentRunState(true) && remove(task)) task.cancel(false); else ensurePrestart(); } }

从源码可以看出,当任务执行完后,如果该任务时周期性任务,那么会重新计算下一次执行时间,然后重新放到任务队列中等待下一次执行。

3. 总结

ScheduledThreadPoolExecutor的实现是基于java线程池。通过对任务进行一层封装来实现任务的周期执行,以及将任务队列改成延迟队列来实现任务的延迟执行。

我们将任务放入任务队列的同时,会尝试开启一个worker来执行这个任务(如果当前worker的数量小于corePoolSize)。由于这个任务队列时一个延迟队列,只有任务执行时间达到才能获取到任务,因此worker只能阻塞等到有队列中有任务到达才能获取到任务执行。

当任务执行完后,会检查自己是否是一个周期性执行的任务。如果是的话,就会重新计算下一次执行的时间,然后重新将自己放入任务队列中。

关于下一次任务的执行时间的计算规则,和Timer差不多,这里就不多做介绍。

三、Timer和ScheduledThreadPoolExecutor的区别

由于Timer是单线程的,如果一次执行多个定时任务,会导致某些任务被其他任务所阻塞。比如A任务每秒执行一次,B任务10秒执行一次,但是一次执行5秒,就会导致A任务在长达5秒都不会得到执行机会。而ScheduledThreadPoolExecutor是基于线程池的,可以动态的调整线程的数量,所以不会有这个问题

如果执行多个任务,在Timer中一个任务的崩溃会导致所有任务崩溃,从而所有任务都停止执行。而ScheduledThreadPoolExecutor则不会。

Timer的执行周期时间依赖于系统时间,timer中,获取到堆顶任务执行时间后,如果执行时间还没到,会计算出需要休眠的时间=(执行时间-系统时间),如果系统时间被调整,就会导致休眠时间无限拉长,后面就算改回来了任务也因为在休眠中而得不到执行的机会。ScheduledThreadPoolExecutor由于用是了nanoTime来计算执行周期的,所以和系统时间是无关的,无论系统时间怎么调整都不会影响到任务调度。

注意的是,nanoTime和系统时间是完全无关的(之前一直以为只是时间戳的纳秒级粒度),关于nanoTime的介绍如下:

返回最准确的可用系统计时器的当前值,以毫微秒为单位。 此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数(或许从以后算起,所以该值可能为负)。此方法提供毫微秒的精度,但不是必要的毫微秒的准确度。它对于值的更改频率没有作出保证。在取值范围大于约 292 年(263 毫微秒)的连续调用的不同点在于:由于数字溢出,将无法准确计算已过的时间。

总体来说,Timer除了在版本兼容性上面略胜一筹以外(Timer是jdk1.3就支持的,而ScheduledThreadPoolExecutor在jdk1.5才出现),其余全部被ScheduledThreadPoolExecutor碾压。所以日常技术选型中,也推荐使用ScheduledThreadPoolExecutor来实现定时任务。

最后,如果哪里有写的不对或者有疑惑的地方,欢迎评论或者邮件我。对Java各种技术有兴趣的也可以加我互相交流。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有