Timer和ScheduledThreadPoolExecutor

2024-09-07 00:38

本文主要是介绍Timer和ScheduledThreadPoolExecutor,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章来源:

https://blog.csdn.net/u013332124/article/details/79603943

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

一、Timer

1. Timer的使用

class MyTask extends TimerTask{@Overridepublic 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 cancelledsynchronized(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会变成falsewhile (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<=currentTime)) {//如果任务的执行周期是0,说明只要执行一次就好了,//就从队列中移除它,这样下一次就不会获取到该任务了if (task.period == 0) {queue.removeMin();task.state = TimerTask.EXECUTED;} else { //重新设置该任务下一次的执行时间//如果之前设置的period小于0,就用当前时间-period,//等于就是当前时间加上周期值//这里的下次执行时间就是当前的执行时间加上周期值//这里涉及到是否以固定频率调用任务的问题,下面再详细讲解queue.rescheduleMin(task.period<0 ? currentTime   - task.period: executionTime + task.period);}}}//如果任务的执行时间还没到,就计算出还有多久才到达执行时间,//然后线程进入休眠if (!taskFired) queue.wait(executionTime - currentTime);}//如果任务的执行时间到了,就执行这个任务if (taskFired)task.run();} catch(InterruptedException e) {}}
}

其中cancel()方法实现如下:

public void cancel() {synchronized(queue) {thread.newTasksMayBeScheduled = false;queue.clear();queue.notify();  // In case queue was already empty.}
}

通过上面的代码,我们大概了解了Timer是怎么工作的了。下面来看一下schedule()方法的相关代码

//Timer.java
public void schedule(TimerTask task, long delay, long period) {if (delay < 0)throw new IllegalArgumentException("Negative delay.");if (period <= 0)throw new IllegalArgumentException("Non-positive period.");//调用内部的一个方法sched(task, System.currentTimeMillis()+delay, -period);
}private void sched(TimerTask task, long time, long period) {if (time < 0)throw new IllegalArgumentException("Illegal execution time.");// 如果设定的定时任务周期太长,就将其除以2if (Math.abs(period) > (Long.MAX_VALUE >> 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() {@Overridepublic void run() {System.out.println("hello world");}
}, 1, 3, TimeUnit.SECONDS);

2. 实现原理+源码解析

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

https://blog.csdn.net/u013332124/article/details/79587436

我们直接来看一下的源码

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) {if (command == null || unit == null)throw new NullPointerException();if (period <= 0)throw new IllegalArgumentException();//将任务进行一层封装,最后得到一个ScheduledFutureTask对象ScheduledFutureTask<Void> sft =new ScheduledFutureTask<Void>(command,null,triggerTime(initialDelay, unit),unit.toNanos(period));//进行一些装饰,其实就是返回sft这个对象RunnableScheduledFuture<Void> t = decorateTask(command, sft);sft.outerTask = t;//提交给线程池执行delayedExecute(t);return t;
}
private void delayedExecute(RunnableScheduledFuture<?> task) {//如果线程池已经关闭,就拒绝这个任务if (isShutdown())reject(task);else {//将当前任务加入到任务队列中去super.getQueue().add(task);//判断线程池是否关闭了,然后判断是否需要移除这个任务if (isShutdown() &&!canRunInCurrentRunState(task.isPeriodic()) &&remove(task))task.cancel(false);else//因为这里的定时任务是直接放到任务队列中,所以需要保证已经有worker启动了ensurePrestart();}
}
void ensurePrestart() {int wc = workerCountOf(ctl.get());//如果worker的数量小于corePoolSize,那就启动一个worker,用来消费任务队列的任务if (wc < corePoolSize)addWorker(null, true);//worker的数量为0也直接启动一个workerelse if (wc == 0)addWorker(null, false);
}

到这里,我们可以看到我们提交的任务被封装成一个ScheduledFutureTask然后提交给任务队列,同时如果发现worker的数量少于设置的corePoolSize,我们还会启动一个worker线程。

但是,我们怎么保证worker不会马上就从任务队列中获取任务然后直接执行呢(这样我们设定的延迟执行就没有效果了)?
另外,怎么保证任务执行完下一次在一定周期后还会再执行呢,也就是怎么保证任务的延迟执行和周期执行?

我们先来看一下任务的延迟执行的解决方案。其实就是修改任务队列的实现,通过将任务队列变成延迟队列,worker不会马上获取到任务队列中的任务了。只有任务的时间到了,worker线程才能从延迟队列中获取到任务并执行。

在ScheduledThreadPoolExecutor中,定义了DelayedWorkQueue类来实现延迟队列。DelayedWorkQueue内部使用了最小堆的数据结构,当任务插入到队列中时,会根据执行的时间自动调整在堆中的位置,最后执行时间最近的那个会放在堆顶。

当worker要去队列获取任务时,如果堆顶的执行时间还没到,那么worker就会阻塞一定时间后才能获取到那个任务,这样就实现了任务的延迟执行。

由于篇幅问题,DelayedWorkQueue的源码就不作解析了,有兴趣的朋友可以去ScheduledThreadPoolExecutor类中查阅。
解决了任务的延迟执行问题,接下来就是任务的周期执行的解决方案了。周期执行和前面封装的ScheduledFutureTask有关。我们直接来看一下ScheduledFutureTask的run方法就知道了

public void run() {//先判断任务是否周期执行boolean periodic = isPeriodic();//判断是否能执行任务if (!canRunInCurrentRunState(periodic))cancel(false);//判断是否周期性任务else if (!periodic)//不是的话执行执行run方法ScheduledFutureTask.super.run();else if (ScheduledFutureTask.super.runAndReset()) {//如果是周期性任务,那就设置下一次的执行时间setNextRunTime();//重新将任务放到队列中,然后等待下一次执行reExecutePeriodic(outerTask);}
}
private void setNextRunTime() {//根据peroid的正负来判断下一次执行时间的计算策略//和timer的下一次执行时间计算策略有点像long p = period;if (p > 0)time += p;elsetime = triggerTime(-p);
}
void reExecutePeriodic(RunnableScheduledFuture<?> task) {//先判断是否可以在当前状态下执行if (canRunInCurrentRunState(true)) {//重新加任务放到任务队列中super.getQueue().add(task);if (!canRunInCurrentRunState(true) && remove(task))task.cancel(false);elseensurePrestart();}
}

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

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来实现定时任务。

这篇关于Timer和ScheduledThreadPoolExecutor的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1143564

相关文章

4.15 版本内核调用 init_timer()函数出错

linux/include/linux/timer.h4.15 之前版本struct timer_list {14 /*15 * All fields that change during normal runtime grouped to the16 * same cacheline17 */18 struct hl

java AWT 绘图,实现弹球游戏,有实现keylistener,timer功能

Timer(int delay, ActionListener listener):每间隔delay毫秒,系统自动触发ActionListener监听器里的事件处理器(actionPerformed()方法)。 package javaAWT;import java.awt.Canvas;import java.awt.Color;import java.awt.Dimension;imp

【大数据Java基础- Java并发 17】J.U.C之线程池:ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor解析 我们知道Timer与TimerTask虽然可以实现线程的周期和延迟调度,但是Timer与TimerTask存在一些缺陷,所以对于这种定期、周期执行任务的调度策略,我们一般都是推荐ScheduledThreadPoolExecutor来实现。下面就深入分析ScheduledThreadPoolExecutor是如何来实现线程的周期、延迟调

Android -- 视频音频多媒体播放,在线播放, MediaPlayer, SurfaceView, SoundPool, Timer定时器使用

1. SoundPool 用于快速重复的播放短音频 [java]  view plain copy public class MainActivity extends Activity {       private SoundPool soundPool;       private int soundId;          @Override       protec

Android开发Timer计时器的操作实例

本文内容讲的是Android开发中Timer计时器如何实现。实际上Timer和TimerTask都是JDK自带的功能,两个类结合可以实现定时任务执行一次,或者定期重复执行任务。 下面模拟一个Android开发Timer计时器实例:   1、新建一个项目:Lesson34_Timer2、MainActivity.java代码如下:   package basic.android.timer

STM32CUBEIDE FreeRTOS操作教程(四):timer软件定时器

STM32CUBEIDE FreeRTOS操作教程(四):timer软件定时器 STM32CUBE开发环境集成了STM32 HAL库进行FreeRTOS配置和开发的组件,不需要用户自己进行FreeRTOS的移植。这里介绍最简化的用户操作类应用教程。以STM32F401RCT6开发板为例,只用到USB,USART1极少的接口,体现FreeRTOS的各种操作过程。 操作教程(四)配置FreeRTO

江山易改本性难移之ZYNQ SDK API函数笔记(Timer)

初学Xilinx ZYNQ SDK的开发,下面记录使用到的API函数及自己的理解。若有误,还请指教。 Timer函数 常用编程步骤: 1.查找输入设备的ID查找设备; 2.输入的配置信息初始化; 3.加载计数周期; 4.设置自动装载模式; 5.启动定时器; 配置GPIO只需要以上5个步骤。 1、XGpioPs_Config *XGpioPs_LookupConfig(u16 Devi

最近用Timer踩了一个坑,分享一下避免别人继续踩

最近做一个小项目,项目中有一个定时服务,需要向对方定时发送数据,时间间隔是1.5s,然后就想到了用C#的Timer类,我们知道Timer 确实非常好用,因为里面有非常人性化的start和stop功能,在Timer里面还有一个Interval,就是用来设置时间间隔,然后时间间隔到了就会触 发Elapsed事件,我们只需要把callback函数注册到这个事件就可以了,如果Interval到了

Java中的定时器(Timer)

目录 一、什么是定时器? 二、标准库中的定时器 三、实现定时器 一、什么是定时器? 定时器就像一个"闹钟",当它到达设定的时间后,就会执行预定的代码。 例如,我们在TCP的超时重传机制中讲过,如果服务器在规定的时间内没有收到客户端返回的ACK确认应答,那么它将再次发送请求。 二、标准库中的定时器 1.标准库中提供了一个Timer类,Timer类的核心方法为sche

Spring+Quartz Java.util.Timer 实现定时任务

一、spring+Quartz实现定时任务的两种简单方式(*.xml配置和注解)一样。  1、 xml文件:  a、配置依赖: b、spring整合quartz c、创建一个Job类  2、注解实现: a、添加项: 二、java.util.Timer实现定时: a、实现: b:分析 java基础里面的java.util.Timer实现定时任务,会应用到java.ut