0%

Java 异步任务执行服务(三):定时任务的那些陷阱

1. 定时任务的应用场景有哪些

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

2. Java 中实现定时任务的方式有

  • 使用 java.util 包中的 TimerTimerTask
  • 使用 Java 并发包中的 ScheduledExecutorService(实践中建议使用)。

3. Timer 和 TimerTask 的区别是

  • TimerTask 表示一个定时任务,是一个抽象类。实现了 Runnable,具体的定时任务需要继承该类,实现 run() 方法。
  • Timer 是一个具体类,负责定时任务的调度和执行。

4. Timer 的主要方法有哪些

  1. public void schedule(TimerTask task, Date time):在指定绝对时间 time 运行任务 task
  2. public void schedule(TimerTask task, long delay):在当前时间延时 delay 毫秒后运行任务 task
  3. public void schedule(TimerTask task, Date firstTime, long period):固定延时重复执行,第一次计划执行时间为 firstTime,后一次的计划执行时间为前一次“实际”执行时间加上 period
  4. public void schedule(Timertask task, long delay, long period):固定延时重复执行,第一次执行时间为当前时间加上 delay
  5. public void scheduledAtFixedRate(TimerTask task, Date firstTime, long period):固定频率重复执行,第一次计划执行时间为 firstTime,后一次的计划执行时间为前一次“计划”执行时间加上 period
  6. public void scheduleAtFixedRate(TimerTask task, long delay, long period):固定频率重复执行,第一次计划执行时间为当前时间加上 delay

5. 固定延时 fixed-delay 和 固定频率 fixed-rate 的区别是

  • 二者都是重复执行,但后一次任务执行相对的时间是不一样的。

    • 对于固定延时,它是基于上次任务的“实际”执行时间来算的,如果由于某种原因,上次任务延时了,则本次任务也会延时。
    • 固定频率会尽量补够运行次数(可能会出现一下子执行很多次任务的情况)。
  • 另外,如果第一次计划执行的时间 firstTime 是一个过去的时间,则任务会立即执行。

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

6. 写一个 Timer 基本 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 对象
timer.schedule(new DelayTask(), 1000); //1 秒钟后运行 DelayTask
Thread.sleep(2000);
timer.cancel(); //取消所有定时任务
}
}

7. 写一个 Timer 固定延时 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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() throws InterruptedException {
Timer timer = new Timer(); //一个 Timer 对象只有一个 Timer 线程,所以运行后可以发现下面第二个任务只有在第一个任务运行结束后才会开始运行
timer.schedule(new LongRunningTask(), 10);
timer.schedule(new FixedDelayTask(), 100, 1000);
}
}

8. 写一个 Timer 固定频率 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TimerFixedRate {
static class LongRunningTask extends TimerTask {
//省略,与 7 一样
}

static class FixedRateTask extends TimerTask {
//省略,与 7 一样
}

public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
timer.schedule(new LongRunningTask(), 10);
timer.scheduleAtFixedRate(new FixedRateTask(), 10, 1000);
}
}

9. Timer的基本原理

  • Timer 内部主要由任务队列和 Timer 线程两部分组成。任务队列是一个基于堆实现的优先级队列,按照下次执行的时间排优先级。Timer 线程负责执行所有的定时任务,需要强调的是,一个 Timer 对象只有一个 Timer 线程。
  • Timer 线程主体是一个循环,从队列中获取任务,如果队列中有任务且计划执行时间小于当前时间,就执行它,如果队列中没有任务或第一个任务延时还没到,就睡眠。如果睡眠过程中队列上添加了新任务且新任务是第一个任务,Timer 线程会被唤醒,重新进行检查。
  • 在执行任务之前,Timer 线程判断任务是否为周期任务。如果是,就设置下次执行的时间并添加到优先级队列中。对于固定延时的任务,下次执行时间为当前时间加上 period;对于固定频率的任务,下次执行时间为上次计划执行时间加上 period
  • 需要强调的是,下次任务的计划是在执行当前任务之前就做出了的。对于固定延时的任务,延时相对的是任务执行前的当前时间,而不是任务执行后。这与后面讲到的 ScheduledExecutorService 的固定延时计算方法是不同的,后者的计算方法更合乎一般的期望;对于固定频率的任务,延时相对的是最先的计划,所以,很有可能出现一下子执行很多次任务的情况
  • 一个 Timer 对象只有一个 Timer 线程。这意味着,定时任务不能耗时太长,更不能是无限循环(infinite loop)。

10. 写一个 Timer 死循环 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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);
}
}

11. Timer 线程的异常处理是怎样的,怎样解决

  • 对于 Timer 线程,在执行任何一个任务的 run() 方法时,一旦 run() 抛出异常Timer 线程就会退出,从而所有定时任务都会被取消
  • 如果希望各个定时任务不互相干扰,一定要在 run() 方法内捕获所有异常

12. 写一个 Timer 异常 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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(); //task B 会抛出异常,导致整个定时任务被取消
}
}

public static void main(String[] args) throws InterruptedException {
Timer timer = new Timer();
timer.schedule(new TaskA(), 1, 1000);
timer.schedule(new TaskB(), 2000, 1000);
}
}
1
2
3
4
5
6
7
8
// 屏幕输出为:
task A
task A
task B
Exception in thread "Timer-0" java.lang.RuntimeException
at ……
at ……
at ……

13. 总结一下 Timer/TimerTask

  • 后台只有一个线程在运行。
  • 固定频率的任务被延迟后,可能会立即执行多次,将次数补够。
  • 固定延时任务的延时相对的是任务执行开始前的时间。
  • 不要在定时任务中使用无限循环,否则会造成死循环
  • 一个定时任务的未处理异常会导致所有定时任务被取消

14. ScheduledExecutorService 的使用场景是

 由于 Timer/TimerTask一些问题,Java 并发包引入了 ScheduledExecutorService

15. ScheduledExecutorService 接口的定义是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//返回类型都是 ScheduledFuture,它是一个接口,扩展了 Future 和 Delayed,没有定义额外方法
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);
}

16. ScheduledExecutorService 的基本用法

  • ScheduledExecutorService 的主要实现类是 ScheduledThreadPoolExecutor,它是线程池 ThreadPoolExecutor 的子类,是基于线程池实现的,主要构造方法与 ThreadPoolExecutor 一样。
  • ScheduledThreadPoolExecutor 的任务队列是一个无界的优先级队列,所以最大线程数对它没有作用,因为线程个数最多只能达到 corePoolSize即使 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):带参数的多线程的定时任务执行服务

17. 写一个多线程的定时任务执行 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ScheduledFixedDelay {
static class LongRunningTask implements Runnable {
//省略,与 TimerFixedDelay 一样
}

static class FixedDelayTask implements Runnable {
//省略,与 TimerFixedDelay 一样
}

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);
}
}

18. 写一个 ScheduledExecutorService 异常 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);
}
}
1
2
3
4
5
6
7
// 输出类似如下:
task A
task A
task B
task A
task A
……
  • 定时任务 Task B 被取消了,但 Task A 不受影响,即使它们是由同一个线程执行的。
  • Timer 不同,没有异常被抛出,Task B 的异常没有在任何地方体现。所以,与 Timer 中的任务类似,应该捕获所有异常。

19. ScheduledThreadPoolExecutor 类、ScheduledExecutorService 接口和 Timer 类、TimerTask 抽象类的相同点和不同点以及共同局限是

  • 相同点ScheduledThreadPoolExecutor 的实现思路与 Timer 基本是类似的,都有一个基于堆的优先级队列,保存待执行的定时任务。

  • 不同点

    • ScheduledExecutorService 接口不支持以绝对时间作为首次运行的时间。
    • ScheduledExecutorService 对于固定的任务,是从任务执行结束后开始算的,即:它在任务执行后再设置下次执行的时间,这对于固定延时的任务更为合理
    • ScheduledThreadPoolExecutor单个定时任务的异常不会再导致整个定时任务被取消,即使后台只有一个线程执行任务。即,任务执行线程会捕获任务执行过程中的所以异常,一个定时任务的异常不会影响其他定时任务。不过,发生异常的任务(即使是一个重复任务)不会再被调度
  • 局限:共同局限是不太胜任复杂的定时任务调度。比如,每周一和周三晚上 18:00 到 22:00,每半小时执行一次。对于类似这种需求,可以利用日期和时间处理方法,或者使用更为强大的第三方类库,比如 Quartz: http://www.quartz-scheduler.org/

20. 并发开发小结

  • 在并发应用程序中,一般我们应该尽量利用高层次的服务,比如各种并发容器任务执行服务线程池等,避免自己管理线程和它们之间的同步。
  • 在个别情况下,自己管理线程及同步又是必需的。这时,除了可以利用 synchronized、显示锁和条件等基本工具,还可以使用 Java 并发包提供的一些高级的同步和协作工具。
-------------------- 本文结束感谢您的阅读 --------------------