为什么你不该用Timer
概述
在Java开发中,用过定时功能的同学一定不会对Timer感到陌生。不过,除了Timer,在Java 5之后又引入了一个定时工具ScheduledThreadPoolExecutor,那么我们应该如何在这两个定时工具之间进行选择呢?
一般情况下我们都建议使用ScheduledThreadPoolExecutor而不是Timer,主要原因有以下3点:
- Timer使用的是绝对时间,系统时间的改变会对Timer产生一定的影响;而ScheduledThreadPoolExecutor使用的是相对时间,所以不会有这个问题。
- Timer使用单线程来处理任务,长时间运行的任务会导致其他任务的延时处理,而ScheduledThreadPoolExecutor可以自定义线程数量。
- Timer没有对运行时异常进行处理,一旦某个任务触发运行时异常,会导致整个Timer崩溃,而ScheduledThreadPoolExecutor对运行时异常做了捕获(可以在
afterExecute()
回调方法中进行处理),所以更加安全。
下面我们就来通过了解Timer与ScheduledThreadPoolExecutor的运行原理来理解上面几个问题出现的原因。
Timer的运行机制
-
TimerTask:任务类。内部持有
nextExecutionTime
变量,表示任务实际执行时间点,单位为毫秒,使用System.currentTimeMillis() + delay
计算得出。 -
TimerQueue:使用小根堆实现的优先队列。按照
TimerTask
的实际执行时间点由小到大排序。 - TimerThread:顾名思义,这是实际执行任务的线程。
TimerThread
会在Timer
初始化后启动,之后会进入mainLoop()
方法,该方法会不断从TimerQueue
中取出时间点最小的TimerTask
。如果该TimerTask
的执行时间点已到,则直接调用TimerTask.run()
执行;否则,调用wait()
方法,等待相应的时间。
而我们调用Timer.schedule()
方法,实际上是通过TimerQueue.add()
方法,将TimerTask
加入任务等待队列。
这里还有一个需要注意的地方是:当加入任务的执行时间点是优先队列中最小的时,就调用notify()
方法唤醒TimerThread
,而TimerThread
在被唤醒后会重新调用TimerQueue.getMin()
方法,再次调用wait()
,不过这次的等待时间就变成了新加入任务的时间点。
ScheduledThreadPoolExecutor的运行机制
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,对线程池的原理不了解的同学,可以看一下我的这篇文章:从零实现ImageLoader(三)—— 线程池详解。
ScheduledThreadPoolExecutor的实现比Timer要复杂一些,不过要是理解了线程池的运行原理,其实也不难。它只不过是在ThreadPoolExecutor的基础上使用自定义的阻塞队列DelayedWorkQueue来实现任务定时功能。所以ScheduledThreadPoolExecutor的运行流程其实和ThreadPoolExecutor是差不多的。
-
ScheduledFutureTask:任务类。内部持有
time
变量,单位为纳秒,通过System.nanoTime() + delay
计算得出。 -
DelayedWorkQueue:使用小根堆实现的优先阻塞队列,将
ScheduledFutureTask
按照从小到大的顺序排列,同时在take()
方法内实现阻塞操作。 -
WorkerThread:这里为了简单起见,我将线程池的核心线程和临时线程统一写成
WorkerThread
,但需要注意的是ScheduledThreadPoolExecutor是线程池的一个子类,所以线程池的那一套东西在ScheduledThreadPoolExecutor里也是有的。
光从这两个图上看,好像ScheduledThreadPoolExecutor和Timer的实现都大同小异,不过是换了一些名字,但实际上这两个的实现还是有很大的不同的,不止因为ScheduledThreadPoolExecutor使用的是多线程。
在Timer里定时功能的实现主要依靠TimerThread.mainLoop()
的等待,而ScheduledThreadPoolExecutor使用的是多线程,在每个线程里都单独实现定时功能是不现实的,因此,ScheduledThreadPoolExecutor将定时功能放在了DelayedWorkQueue
类里,而由于DelayedWorkQueue
是阻塞队列,所以定时任务的实现实际上就在DelayedWorkQueue.take()
方法中。下面我们就来分析一下DelayedWorkQueue.take()
到底做了什么。
Leader/Follower模式
在多线程网络编程中,我们一般使用一个线程监听端口,在接收到事件后再使用其他的线程去完成操作。这种情况下,在两个线程之间的上下文切换开销其实是很大的,于是我们有了Leader/Follower模式:
在Leader/Follower模式中,不存在一个专门用来监听的线程,所有的线程都是等价的,而这些线程会不断在Leader、Follower和Processor这三个状态之间来回切换。
在程序中会保证每个时刻有且只有一个Leader,这个Leader就暂时充当了之前用来监听端口线程的作用。而当有一个新的事件发生时,Leader不再是重新找一个线程去处理连接,而是自己转化为Processor处理事件,并且重新指定一个Follower作为新的Leader。当事件处理完毕后,Processor又会转化为Follower等待重新成为Leader。
take()方法的原理
这里的take()
方法就借助了Leader/Follower模式的思想,同一时刻只有一个Leader线程,不过这里由于任务执行的时间点是已经确定了的,所以不再是等待一个触发事件,而是等待最小任务所对应的延迟时间。其他的Follower线程则处于无限等待的状态,直到当前Leader到达指定时间后转化为Processor去处理任务,这时就会唤醒一个Follower作为下一任的Leader。而Processor在处理完任务后又会重新加入Follower进行等待。
绝对时间与相对时间
了解了Timer与ScheduledThreadPoolExecutor的运行机制,下面我们就来看一下Timer的这些缺陷究竟是怎么回事。
首先是绝对时间与相对时间的问题,可能有人已经发现,不管是TimerTask还是ScheduledFutureTask都是存储的实际执行时间点,只不过一个是毫秒,一个是纳秒,难道时间单位还会对这些有影响?确实,时间单位是不会对任务的执行有影响的,不过这里的玄机就在于这个时间的计算方式:System.currentTimeMillis()
与System.nanoTime()
。
System.currentTimeMillis()
大家已经很清楚了,就是当前时间与1970年1月1日午夜的时间差的毫秒数,而System.nanoTime()
又是什么呢?官方文档里是这么说的:
此方法只能用于测量已过的时间,与系统或钟表时间的其他任何时间概念无关。返回值表示从某一固定但任意的时间算起的毫微秒数。
这就是Timer与ScheduledThreadPoolExecutor一个是基于绝对时间而另一个是基于相对时间的原因。下面我们写个例子来测试一下:
public static void main(String[] args) {
System.out.println("Start:\t" + new Date());
Executors.newSingleThreadScheduledExecutor().schedule(() -> {
System.out.println("Executor:\t" + new Date());
}, 60, TimeUnit.SECONDS);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer:\t" + new Date());
}
}, 60000);
}
输出:
Start: Sun Oct 08 10:51:44 CST 2017
Executor: Sun Oct 08 10:51:41 CST 2017
Timer: Sun Oct 08 10:52:45 CST 2017
这里,我在启动之后将系统的时钟向后调了一分钟,所以实际的启动时间应该是10:50:44
,由于ScheduledThreadPoolExecutor的等待时间与系统无关,所以在一分钟后执行;而Timer是基于绝对时间的所以在10:52:45
执行,实际上这时已经过去两分钟了。
单线程与多线程
Timer的第二个缺陷是,由于它使用的是单线程,所以长时间执行的任务会对其他任务产生影响。
public static void main(String[] args) {
System.out.println("Start:\t\t\t" + new Date());
ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
service.schedule(() -> {
System.out.println("Executor 任务1:\t" + new Date());
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 60, TimeUnit.SECONDS);
service.schedule(() -> {
System.out.println("Executor 任务2:\t" + new Date());
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, 60, TimeUnit.SECONDS);
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer 任务1:\t\t" + new Date());
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 60000);
timer.schedule(new TimerTask() {
@Override
public void run() {
System.out.println("Timer 任务2:\t\t" + new Date());
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, 60000);
}
输出:
Start: Sun Oct 08 11:10:34 CST 2017
Executor 任务1: Sun Oct 08 11:11:34 CST 2017
Executor 任务2: Sun Oct 08 11:11:34 CST 2017
Timer 任务1: Sun Oct 08 11:11:34 CST 2017
Timer 任务2: Sun Oct 08 11:12:04 CST 2017
可以看到ScheduledThreadPoolExecutor中的两个任务在等待一分钟之后同时执行;而在Timer中的任务2却因任务1长达半分钟的执行时间,总共等了一分半钟才得以执行。
异常处理
最后我们来看一下Timer与ScheduledThreadPoolExecutor对异常的处理情况:
Timer
Timer内部没有对异常做任何处理,如果任务执行发生运行时异常,整个TimerThread都会崩溃:
public static void main(String[] args) {
System.out.println("Start:\t\t\t" + new Date());
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
throw new RuntimeException("Timer 任务1");
}
}, 60000);
timer.schedule(new TimerTask() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Timer 任务2:\t\t" + new Date());
}
}, 60000);
}
输出:
Start: Sun Oct 08 11:53:05 CST 2017
Exception in thread "Timer-0" java.lang.RuntimeException: Timer 任务1
at main.Main$1.run(Main.java:32)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
可以看到,任务1抛出的运行时异常导致整个Timer线程崩溃,任务2自然也没有执行。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor中对异常的处理实际上是ThreadPoolExecutor类完成的,ThreadPoolExecutor在任务运行时对异常做了捕获,并且将异常传入了afterExecute()
方法:
public class ThreadPoolExecutor extends AbstractExecutorService {
final void runWorker(Worker w) {
...
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
...
}
}
我们来验证一下:
public static void main(String[] args) {
System.out.println("Start:\t\t\t" + new Date());
ScheduledExecutorService service = Executors.newSingleThreadScheduledExecutor();
service.schedule(() -> {
throw new RuntimeException("Executor 任务1");
}, 60, TimeUnit.SECONDS);
service.schedule(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Executor 任务2:\t" + new Date());
}, 60, TimeUnit.SECONDS);
}
输出:
Start: Sun Oct 08 11:33:35 CST 2017
Executor 任务2: Sun Oct 08 11:34:36 CST 2017
可以看到这里虽然任务1抛出了运行时异常,但由于线程池内部完善的异常处理机制,任务2得以成功执行。
后记
看了这么多Timer的缺陷,你还在犹豫吗?赶快放弃Timer,投入ScheduledThreadPoolExecutor的怀抱吧!