并发编程-——-Timer-源码分析

## 前言

在平时的开发中,肯定需要使用定时任务,而 Java 1.3 版本提供了一个 java.util.Timer 定时任务类。今天一起来看看这个类。

1.API 介绍

Timer 相关的有 3 个类:

Timer :面向程序员的API 都在这个类中。
TaskQuue: 存储任务。
TimerThread: 执行任务的线程。

这个类的构造方法有 4 个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Timer()                               创建一个新计时器。
Timer(boolean isDaemon) 创建一个新计时器,可以指定其相关的线程作为守护程序运行。
Timer(String name) 创建一个新计时器,其相关的线程具有指定的名称。
Timer(String name, boolean isDaemon) 创建一个新计时器,其相关的线程具有指定的名称,并且可以指定作为守护程序运行。
````

程序员可以使用的 API 如下:
```java
void cancel() 终止此计时器,丢弃所有当前已安排的任务。
int purge() 从此计时器的任务队列中移除所有已取消的任务。
void schedule(TimerTask task, Date time) 安排在指定的时间执行指定的任务。
void schedule(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定延迟执行。
void schedule(TimerTask task, long delay) 安排在指定延迟后执行指定的任务。
void schedule(TimerTask task, long delay, long period) 安排指定的任务从指定的延迟后开始进行重复的固定延迟执行。
void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 安排指定的任务在指定的时间开始进行重复的固定速率执行。
void scheduleAtFixedRate(TimerTask task, long delay, long period) 安排指定的任务在指定的延迟后开始进行重复的固定速率执行。

下面从几个具有代表性的方法开始分析 Timer 的源码。

2. 从构造方法开始

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Timer timer = new Timer();

public Timer() {
this("Timer-" + serialNumber());
}

public Timer(String name) {
thread.setName(name);
thread.start();
}

/**
* The timer thread.
*/
private final TimerThread thread = new TimerThread(queue);

private final TaskQueue queue = new TaskQueue();

private TimerTask[] queue = new TimerTask[128];

从上面一连串的构造方法中,可以看出,Timer 内部使用了一个线程 TimerThread,线程的构造参数是一个队列(数组)。

然后直接启动了这个线程,默认是非守护模式的。

而这个线程的 run 方法又是如何的呢?

1
2
3
4
5
6
7
8
9
10
11
public void run() {
try {
mainLoop();
} finally {
// Someone killed this Thread, behave as if Timer cancelled
synchronized(queue) {
newTasksMayBeScheduled = false;
queue.clear(); // Eliminate obsolete references
}
}
}

主要执行 mainLoop 方法,当任务结束后,清除队列。并不在接受新的任务。

那么这个 mainLoop 方法的逻辑是什么呢?猜想一下,肯定是执行队列中的任务。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
private void mainLoop() {
while (true) {
try {
TimerTask task;
boolean taskFired;
synchronized(queue) {
// 如果队列是空 且 newTasksMayBeScheduled 是 true,阻塞等待
while (queue.isEmpty() && newTasksMayBeScheduled)
queue.wait();
// 如果被唤醒了,且队列还是空,跳出循环结束。
if (queue.isEmpty())
break; // Queue is empty and will forever remain; die

// Queue nonempty; look at first evt and do the right thing
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)) {
// 且任务是不重复的
if (task.period == 0) { // Non-repeating, remove
// 删除这个任务
queue.removeMin();
// 修改状态
task.state = TimerTask.EXECUTED;
} else { // Repeating task, reschedule
// 如果任务是重复的。重新调度任务时间,以便下次执行。
queue.rescheduleMin(
task.period<0 ? currentTime - task.period
: executionTime + task.period);
}
}
}
// 如果时间没到,就等代指定时间
if (!taskFired) // Task hasn't yet fired; wait
queue.wait(executionTime - currentTime);
}
// 如果时间到了,就执行任务。
if (taskFired) // Task fired; run it, holding no locks
task.run();
} catch(InterruptedException e) {
// 如果有中断异常就忽略。
}
}
}

一如既往,写了很多注释,简单说说逻辑:

  1. 死循环并锁住队列,因为这个 Timer 对象可能会被多个线程使用。
  2. 从队列中取出任务。如果任务是重复执行的,就重新设置任务的执行时间。
  3. 执行任务的 run 方法。

这里有几个注意的地方:

  1. 该方法忽略了线程中断异常。当 wait 方法中断异常的时候,是不起作用的。
  2. 该方法值只捕获线程中断异常,如果发生了其他异常,整个 Timer 就会停止。

So,一定不要在自己的任务里抛出异常,否则一定会影响整个定时任务。

3. schedule 方法

1
timer.schedule(new MyTask(), 1000, 2000);

以上定义了一个任务,1 秒后执行,重复执行时间 2 秒。

schedule 代码如下:

1
2
3
4
5
6
7
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);
}

在 dealy 时间的基础上,加上了当前时间,将 period 变成负数。

看看 sched 方法实现:

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
30
31
private void sched(TimerTask task, long time, long period) {
if (time < 0)
throw new IllegalArgumentException("Illegal execution time.");

// 防止数值溢出
if (Math.abs(period) > (Long.MAX_VALUE >> 1))
period >>= 1;

synchronized(queue) {
// 如果该变量是 false ,说明任务线程停止了,抛出异常
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();
}
}

总结一下该方法,将任务添加进队列,如果调度线程结束了,就抛出异常—— 不能再添加。如果添加成功之后,获取到的第一个任务就是这个任务,说明调度线程阻塞了,那就唤醒他。

4.总结

从一个定时任务的角度讲,Timer 非常的简单,使用一个线程,使用一个队列。在简单的场合,Timer 确实能够满足需求,但 Timer 还是有很多的缺陷:

  1. 不能 catch 住非线程中断异常,如果用户任务异常,将会导致整个 Timer 停止。

  2. 默认情况下不是守护线程,也就是说,他会阻止应用程序停止。你可以使用 cancel 方法停止他。

  3. 如果 Timer 因为 stop 方法获取用户任务异常终止了,那么将再也不能向队列中添加任务了。否则抛出异常。

  4. 如果某个任务的执行时间太长,那么他将会 “独占” 计时器的任务执行现场。导致延迟后续任务的执行,并且会将任务 “堆” 在一起。

So, 大规模的生产环境中,不建议使用 Timer,而是使用 JUC 的 ScheduledThreadPoolExecutor。楼主将在后面的文章中分析 ScheduledThreadPoolExecutor 的实现,相比较 Timer 有什么好处。


并发编程-——-Timer-源码分析
http://thinkinjava.cn/2018/04/20/2018/2018-04-20-并发编程-——-Timer-源码分析/
作者
莫那·鲁道
发布于
2018年4月20日
许可协议