本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


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

  • 闹钟程序或任务提醒,指定时间叫床或在指定日期提醒还信用卡
  • 监控系统,每隔一段时间采集下系统数据,对异常事件报警
  • 统计系统,一般凌晨一定时间统计昨日的各种数据指标

在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 <V> ScheduledFuture<V> schedule(Callable<V> 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);
}
}

TaskA和TaskB都是每秒执行一次,TaskB两秒后执行,但一执行就抛出异常,屏幕的输出类似如下:

task A
task A
task B
task A
task A
...

这说明,定时任务TaskB被取消了,但TaskA不受影响,即使它们是由同一个线程执行的。不过,需要强调的是,与Timer不同,没有异常被抛出来,TaskB的异常没有在任何地方体现。所以,与Timer中的任务类似,应该捕获所有异常。

基本原理

ScheduledThreadPoolExecutor的实现思路与Timer基本是类似的,都有一个基于堆的优先级队列,保存待执行的定时任务,它的主要不同是:

  • 它的背后是线程池,可以有多个线程执行任务
  • 它在任务执行后再设置下次执行的时间,对于固定延时的任务更为合理
  • 任务执行线程会捕获任务执行过程中的所有异常,一个定时任务的异常不会影响其他定时任务,但发生异常的任务也不再被重新调度,即使它是一个重复任务

小结

本节介绍了Java中定时任务的两种实现方式,Timer和ScheduledExecutorService,需要特别注意Timer的一些陷阱,实践中建议使用ScheduledExecutorService。

它们的共同局限是,不太胜任复杂的定时任务调度,比如,每周一和周三晚上18:00到22:00,每半小时执行一次。对于类似这种需求,可以利用我们之前在32节33节介绍的日期和时间处理方法,或者利用更为强大的第三方类库,比如Quartz(http://www.quartz-scheduler.org/)。

在并发应用程序中,一般我们应该尽量利用高层次的服务,比如前面章节介绍的各种并发容器、任务执行服务线程池等,避免自己管理线程和它们之间的同步,但在个别情况下,自己管理线程及同步是必需的,这时,除了利用前面章节介绍的synchronized, wait/notify, 显示锁条件等基本工具,Java并发包还提供了一些高级的同步和协作工具,以方便实现并发应用,让我们下一节来了解它们。

(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic)

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (80) - 定时任务的那些坑的更多相关文章

  1. 《Java编程的逻辑》 - 文章列表

    <计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...

  2. Java编程的逻辑 (83) - 并发总结

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  3. Java编程的逻辑 (85) - 注解

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  4. Java编程的逻辑 (79) - 方便的CompletionService

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  5. Java编程的逻辑 (6) - 如何从乱码中恢复 (上)?

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  6. Java编程的逻辑 (7) - 如何从乱码中恢复 (下)?

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  7. Java编程的逻辑 (9) - 条件执行的本质

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  8. Java编程的逻辑 (36) - 泛型 (中) - 解析通配符

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  9. Java编程的逻辑 (57) - 二进制文件和字节流

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

随机推荐

  1. go语言学习-数组-切片-map

    数组 go语言中数组的特点: 数组的长度是固定的,并且长度也是数组类型的一部分 是值类型,在赋值或者作为参数传递时,会复制整个数组,而不是指针 定义数组的语法: var arr1 = [5]int{1 ...

  2. [代码审计]某租车系统JAVA代码审计[前台sql注入]

    0x00 前言 艰难徘徊这么久,终于迈出第一步,畏畏缩缩是阻碍大多数人前进的绊脚石,共勉. 系统是租车系统,这个系统是Adog师傅之前发在freebuf(http://www.freebuf.com/ ...

  3. Object类浅析

    Object类的方法有: hashCode(): 返回该对象的哈希码值 hashCode 的常规协定是: 在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回 ...

  4. CodeForces903G Yet Another Maxflow Problem 扫描线 + 线段树 + 最小割

    给定两条链\(A, B\),其中\(A\)链某些点向\(B\)链有连边,支持修改\(A\)链中的某条边权以及查询\(A_1\)到\(B_n\)的最大流 显而易见,\(A\)和\(B\)链中一定满足左部 ...

  5. memcache bug

    用memcached有时会报错  the lowest two bytes of the flags array is reserved for pecl/memcache 将以前的 $mc-> ...

  6. 设置Tab键缩进2字符

    默认是1.74cm,是1.5字符. 论文是4个空格显示为4个点,或者全角下2个空格,显示为2个空格框.看上方的首航缩进标志是否在2. 看图 在段落 格式里面进行设置! 点上图的制表位 打开下图 选默认 ...

  7. AngularJS自定义Directive中link和controller的区别

    在AngularJS中,自定义Directive过程中,有时用link和controller都能实现相同的功能.那么,两者有什么区别呢? 使用link函数的Directive 页面大致是: <b ...

  8. 什么叫做GNU

    GNU就是GNU's Not Unix的缩写, GNU 的创始人Stallman 认为UNIX 虽然不是最 好的操作系统,但是至少不会太差,而他自信有能力把UNIX不足的地方加以改进,使它 成为一个优 ...

  9. angular default project (angular.json的解读)

    Change the default Angular project Understanding it's purpose and limits Klaus KazlauskasFollow Nov ...

  10. ThinikPhp 将数据库模型的增、删、改操作写入日志

    Thinkphp中的模型可以对数据库字段进行验证规则的设置和设置一些字段的默认值(比如字段为当前时间)以及在操作数据时的的一些回调方法等   基本上每一个模型都需要设置一些验证规则和字段默认值的设置, ...