专栏名称: 老马说编程
从入门到高级, 深入浅出, 老马和你一起探索编程及计算机技术的本质, 篇篇原创, 用心写作。
目录
相关文章推荐
码农翻身  ·  Bill Gates 和 Linus ... ·  3 小时前  
程序员的那些事  ·  马斯克狂吹的 Grok 3 ... ·  15 小时前  
程序员的那些事  ·  字节用 8266.8 ... ·  3 天前  
码农翻身  ·  DeepSeek彻底爆了! ·  昨天  
程序猿  ·  微信+DeepSeek来了! ·  2 天前  
51好读  ›  专栏  ›  老马说编程

(80) 定时任务的那些坑 / 计算机程序的思维逻辑

老马说编程  · 公众号  · 程序员  · 2017-03-16 21:44

正文

查看历史文章,请点击上方链接关注公众号。



本节探讨定时任务,定时任务的应用场景是非常多的,比如:

  • 闹钟程序或任务提醒,指定时间叫床或在指定日期提醒还信用卡

  • 监控系统,每隔一段时间采集下系统数据,对异常事件报警

  • 统计系统,一般凌晨一定时间统计昨日的各种数据指标


在Java中,有两种方式实现定时任务:

  • 使用java.util包中的Timer和TimerTask

  • 使用Java并发包中的ScheduledExecutorService


它们的基本用法都是比较简单的,但如果对它们没有足够的了解,则很容易陷入其中的一些陷阱,下面,我们就来介绍它们的用法、原理以及那些坑。

Timer和TimerTask
基本用法
TimerTask表示一个定时任务,它是一个抽象类,实现了Runnable,具体的定时任务需要继承该类,实现run方法。

Timer是一个具体类,它负责定时任务的调度和执行,它有如下主要方法:

// 在指定绝对时间time 运行任务task
public void schedule(TimerTask task, Date time)
// 在当前时间延时delay毫秒 后运行任务task
public void schedule(TimerTask task, long delay)
// 固定延时重复执行 ,第一次计划执行时间为firstTime, 后一次的计划执行时间为前一次"实际"执行时间加上period
public void schedule(TimerTask task, Date firstTime, long period)
//同样是固定延时重复执行,第一次执行时间为当前时间加上delay
public void schedule(TimerTask task, long delay, long period)
/ /固定频率重复执行 ,第一次计划执行时间为firstTime, 后一次的计划执行时间为前一次"计划"执行时间加上period
public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
//同样是固定频率重复执行,第一次计划执行时间为当前时间加上delay
public void scheduleAtFixedRate(TimerTask task, long delay, long period)


需要注意固定延时(fixed-delay)与固定频率(fixed-rate)的区别,都是重复执行,但后一次任务执行相对的时间是不一样的,对于 固定延时,它是基于上次任务的"实际" 执行时间来算的 ,如果由于某种原因,上次任务延时了,则本次任务也会延时, 而固定频率会尽量补够运行次数

另外,需要注意的是,如果第一次计划执行的时间firstTime是一个过去的时间,则任务会立即运行,对于固定延时的任务,下次任务会基于第一次执行时间计算,而对于固定频率的任务,则会从firstTime开始算,有可能加上period后还是一个过去时间,从而连续运行很多次,直到时间超过当前时间。


我们通过一些简单的例子具体来看下。


基本示例
看一个最简单的例子:

public class BasicTimer {
static class DelayTask extends TimerTask {

@Override
public void run() {
System.out.println("delayed task");
}
}

public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
timer.schedule(new DelayTask(), 1000);
Thread.sleep(2000);
timer.cancel();
}
}


创建一个Timer对象,1秒钟后运行DelayTask,最后调用Timer的cancel方法取消所有定时任务。

看一个固定延时的简单例子:

public class TimerFixedDelay {

static class LongRunningTask extends TimerTask {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println("long running finished");
}
}

static class FixedDelayTask extends TimerTask {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}

public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();

timer.schedule(new LongRunningTask(), 10);
timer.schedule(new FixedDelayTask(), 100, 1000);
}
}


有两个定时任务,第一个运行一次,但耗时5秒,第二个是重复执行,1秒一次,第一个先运行。运行该程序,会发现,第二个任务只有在第一个任务运行结束后才会开始运行,运行后1秒一次。

如果替换上面的代码为固定频率,即代码变为:

public class TimerFixedRate {

static class LongRunningTask extends TimerTask {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println("long running finished");
}
}

static class FixedRateTask extends TimerTask {

@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}

public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();

timer.schedule(new LongRunningTask(), 10);
timer. scheduleAtFixedRate (new FixedRateTask(), 100, 1000);
}
}


运行该程序,第二个任务同样只有在第一个任务运行结束后才会运行,但它会把之前没有运行的次数补过来,一下子运行5次,输出类似下面这样:

long running finished
1489467662330
1489467662330
1489467662330
1489467662330
1489467662330

1489467662419
1489467663418


基本原理
Timer内部主要由两部分组成,任务队列和Timer线程。任务队列是一个基于堆实现的优先级队列,按照下次执行的时间排优先级。Timer线程负责执行所有的定时任务, 需要强调的是,一个Timer对象只有一个Timer线程 ,所以,对于上面的例子,任务才会被延迟。

Timer线程主体是一个循环,从队列中拿任务,如果队列中有任务且计划执行时间小于等于当前时间,就执行它,如果队列中没有任务或第一个任务延时还没到,就睡眠。如果睡眠过程中队列上添加了新任务且新任务是第一个任务,Timer线程会被唤醒,重新进行检查。


在执行任务之前,Timer线程判断任务是否为周期任务,如果是,就设置下次执行的时间并添加到优先级队列中,对于固定延时的任务,下次执行时间为 当前时间 加上period,对于固定频率的任务,下次执行时间为 上次计划执行时间 加上period。

需要强调是,下次任务的计划是在执行当前任务之前就做出了的, 对于固定延时的任务,延时相对的是任务执行前的当前时间 ,而不是任务执行后,这与后面讲到的ScheduledExecutorService的固定延时计算方法是不同的,后者的计算方法更合乎一般的期望。

另一方面, 对于固定频率的任务,它总是基于最先的计划计划的 ,所以,很有可能会出现前面例子中一下子执行很多次任务的情况。


死循环

一个Timer对象只有一个Timer线程,这意味着, 定时任务不能耗时太长,更不能是无限循环 ,看个例子:

public class EndlessLoopTimer {
static class LoopTask extends TimerTask {

@Override
public void run() {
while (true) {
try {
// ... 执行任务
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

// 永远也没有机会执行
static class ExampleTask extends TimerTask {
@Override
public void run() {

System.out.println("hello");
}
}

public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
timer.schedule(new LoopTask(), 10);
timer.schedule(new ExampleTask(), 100);
}
}


第一个定时任务是一个无限循环,其后的定时任务ExampleTask将永远没有机会执行。


异常处理
关于Timer线程,还 需要强调非常重要的一点,在执行任何一个任务的run方法时,一旦run抛出异常,Timer线程就会退出,从而所有定时任务都会被取消 。我们看个简单的示例:

public class TimerException {

static class TaskA extends TimerTask {

@Override
public void run() {
System.out.println("task A");
}
}

static class TaskB extends TimerTask {

@Override
public void run() {
System.out.println("task B");
throw new RuntimeException();
}
}

public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
timer.schedule(new TaskA(), 1, 1000);
timer.schedule(new TaskB(), 2000, 1000);
}
}


期望TaskA每秒执行一次,但TaskB会抛出异常,导致整个定时任务被取消,程序终止,屏幕输出为:

task A
task A
task B
Exception in thread "Timer-0" java.lang.RuntimeException
at laoma.demo.timer.TimerException$TaskB.run(TimerException.java:21)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)


所以,如果希望各个定时任务不互相干扰, 一定要在run方法内捕获所有异常

小结
可以看到,Timer/TimerTask的基本使用是比较简单的,但我们需要注意:

  • 背后只有一个线程在运行

  • 固定频率的任务被延迟后,可能会立即执行多次,将次数补够

  • 固定延时任务的延时相对的是任务执行前的时间

  • 不要在定时任务中使用无限循环

  • 一个定时任务的未处理异常会导致所有定时任务被取消


ScheduledExecutorService
接口和类定义
由于Timer/TimerTask的一些问题,Java并发包引入了ScheduledExecutorService,它是一个接口,其定义为:

public interface ScheduledExecutorService extends ExecutorService {
//单次执行,在指定延时delay后运行command
public ScheduledFuture> schedule (Runnable command, long delay, TimeUnit unit);
//单次执行,在指定延时delay后运行callable
public ScheduledFuture schedule (Callable callable, long delay, TimeUnit unit);
//固定频率重复执行
public ScheduledFuture> scheduleAtFixedRate (Runnable command, long initialDelay, long period, TimeUnit unit);
//固定延时重复执行
public ScheduledFuture> scheduleWithFixedDelay (Runnable command, long initialDelay, long delay, TimeUnit unit);
}


它们的返回类型都是ScheduledFuture,它是一个接口,扩展了Future和Delayed,没有定义额外方法。这些方法的大部分语义与Timer中的基本是类似的。对于固定频率的任务,第一次执行时间为initialDelay后,第二次为initialDelay+period,第三次initialDelay+2*period,依次类推。不过, 对于固定延时的任务,它是从任务执行后开始算的 ,第一次为initialDelay后,第二次为第一次任务执行结束后再加上delay。与Timer不同,它不支持以绝对时间作为首次运行的时间。

ScheduledExecutorService的主要实现类是ScheduledThreadPoolExecutor,它是线程池ThreadPoolExecutor的子类,是基于线程池实现的,它的主要构造方法是:

public ScheduledThreadPoolExecutor(int corePoolSize)


此外,还有构造方法可以接受参数ThreadFactory和RejectedExecutionHandler,含义与ThreadPoolExecutor一样,我们就不赘述了。

它的任务队列是一个无界的优先级队列,所以最大线程数对它没有作用,即使corePoolSize设为0,它也会至少运行一个线程。

工厂类Executors也提供了一些方便的方法,以方便创建ScheduledThreadPoolExecutor,如下所示:

//单线程的定时任务执行服务
public static ScheduledExecutorService newSingleThreadScheduledExecutor ()
public static ScheduledExecutorService newSingleThreadScheduledExecutor (ThreadFactory threadFactory)
//多线程的定时任务执行服务
public static ScheduledExecutorService newScheduledThreadPool (int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool (int corePoolSize, ThreadFactory threadFactory)


基本示例
由于可以有多个线程执行定时任务,一般任务就不会被某个长时间运行的任务所延迟了,比如,对于前面的TimerFixedDelay,如果改为:

public class ScheduledFixedDelay {
static class LongRunningTask implements Runnable {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
}
System.out.println("long running finished");
}
}

static class FixedDelayTask implements Runnable {
@Override
public void run() {
System.out.println(System.currentTimeMillis());
}
}

public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService timer = Executors.newScheduledThreadPool(10);
timer.schedule(new LongRunningTask(), 10, TimeUnit.MILLISECONDS);
timer.scheduleWithFixedDelay(new FixedDelayTask(), 100, 1000,
TimeUnit.MILLISECONDS);
}
}


再次执行,第二个任务就不会被第一个任务延迟了。

另外,与Timer不同,单个定时任务的异常不会再导致整个定时任务被取消了,即使背后只有一个线程执行任务,我们看个例子:

public class ScheduledException {

static class TaskA implements Runnable {

@Override
public void run() {
System.out.println("task A");
}
}

static class TaskB implements Runnable {

@Override
public void run() {
System.out.println("task B");
throw new RuntimeException();
}
}

public static void main(String[] args) throws InterruptedException {
ScheduledExecutorService timer = Executors
.newSingleThreadScheduledExecutor();
timer.scheduleWithFixedDelay(new TaskA(), 0, 1, TimeUnit.SECONDS);
timer.scheduleWithFixedDelay(new TaskB(), 2, 1, TimeUnit.SECONDS);
}
}







请到「今天看啥」查看全文