1、任务精确性java
经过前两节的分析,大概知道了Timer的运行原理,下面说说使用Timer须要注意的一些事项。下面是Timer简单原理图linux
从上图能够看到,真正运行闹钟的是一个单线程。也就是说队列中的闹钟,只能依次进行串行化的操做,闹钟的定时执行得不到保证。api
好比下面的例子(本节全部代码只列出关键部分,下同)并发
public class ScheduleDemo { public static void main(String[] args) throws Exception { Timer timer = new Timer(); timer.schedule(new AlarmTask("闹钟"),1000,2000); } static class AlarmTask extends TimerTask { public void run() { log.info(new Date() +" 嘀。。。"); Thread.sleep(10_000); //模拟闹钟执行时间 } } }
从下面的运行结果能够看到,预期2秒之后运行的闹钟,推迟到了10秒之后。异步
Fri Nov 16 14:49:39 CST 2018 嘀。。。 Fri Nov 16 14:49:49 CST 2018 嘀。。。
下面是闹钟运行的时序图jvm
解决方法ide
针对上面的状况,用户可在AlarmTask.run()里面再开一个异步线程,让TimerThread及时返回,执行队列中后续的闹钟。oop
public class ScheduleDemo { public static void main(String[] args) throws Exception { Timer timer = new Timer(); timer.schedule(new AlarmTask("闹钟"),1000,2000); } static class AlarmTask extends TimerTask{ static ExecutorService threadPool = Executors.newCachedThreadPool(); public void run() { // 创建线程池,提升线程的复用,避免线程建立与上下文切换所带来的开销 threadPool.execute(new Runnable() { public void run() { log.info(new Date()+" 嘀。。。"); Thread.sleep(10_000); //模拟闹钟执行时间 } }); } } }
从下面的运行结果能够看到,全部的闹钟执行间隔符合预期的2秒。性能
Fri Nov 16 15:37:59 CST 2018 嘀。。。 Fri Nov 16 15:38:01 CST 2018 嘀。。。 Fri Nov 16 15:38:03 CST 2018 嘀。。。 Fri Nov 16 15:38:05 CST 2018 嘀。。。 Fri Nov 16 15:38:07 CST 2018 嘀。。。 Fri Nov 16 15:38:09 CST 2018 嘀。。。
下面是异步执行的时序图ui
经过异步执行任务的方式虽然保证了执行时间的准确性,但也会出现如下问题:
1. 操做系统通常对线程总量加以限制,好比linux下的/proc/sys/kernel/threads-max。当系统并发量很高的时候,开异步会影响其余应用的线程使用。
2. 若是当前系统运行着计算密度型应用,在CPU使用率很高的状况下将会出现排队现象。
3. JVM会给每个线程分配栈内存,若是Timer分配的任务过多,将很快出现内存溢出的状况。
2、内存泄漏
第二个须要注意的问题是,当用户取消了一个任务之后,失效的任务依然会占据着queue队列,形成内存泄漏,下面是取消任务的源码。
public abstract class TimerTask implements Runnable { final Object lock = new Object(); int state = VIRGIN; static final int CANCELLED = 3; public boolean cancel() { synchronized(lock) { boolean result = (state == SCHEDULED); state = CANCELLED; return result; } }
能够看到TimerTask.cancel()仅仅只是修改task的状态值,并无及时清理失效的任务。纵观整个Timer源码,惟一进行自我清理是在TimerThread中维护的(前提是当前失效的任务优先级最高)。
class TimerThread extends Thread { private TaskQueue queue; public void run() { mainLoop(); } private void mainLoop() { while (true) { synchronized(queue) { task = queue.getMin(); synchronized(task.lock) { if (task.state == TimerTask.CANCELLED) { // 整个Timer中惟一维护自我清理的地方 queue.removeMin(); continue; } } } } } }
下面列举一个内存泄漏的例子。
public class ScheduleDemo { public static void main(String[] args) throws Exception { Timer timer = new Timer(); int i = 0; timer.schedule(new AlarmTask("闹钟"+i++),100,100); while(true){ TimerTask alarm = new AlarmTask("闹钟"+i); timer.schedule(alarm,100,10_0000); alarm.cancel(); Thread.yield(); log.info("已取消闹钟"+i++); } } static class AlarmTask extends TimerTask{ String name ; byte[] bytes = new byte[10*1024*1024]; //模拟业务数据 public AlarmTask(String name){ this.name=name; } @Override public void run() { log.info("["+name+"]嘀。。。"); } } }
为了快速暴露问题,特地增长了闹钟实例的大小;同时限制了jvm的堆内存分配
-Xmx100M -Xms100M
运行结果以下
已取消闹钟1 已取消闹钟2 已取消闹钟3 已取消闹钟4 已取消闹钟5 已取消闹钟6 已取消闹钟7 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.haanoo.schedule.ScheduleDemo$AlarmTask.<init>(ScheduleDemo.java:25) at com.haanoo.schedule.ScheduleDemo.main(ScheduleDemo.java:15) [闹钟0]嘀。。。 [闹钟0]嘀。。。
从运行的结果看出,失效闹钟没有被及时清理,且很快形成了OOM(主线程因OOM异常退出,而TimerThread线程不受影响)。
有人会想:会不会GC没有运行,或来不及运行而致使OOM?下面看一下GC日志,同时dump一下OOM时的堆内存,方便后面MAT分析
-XX:+PrintGC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/timer.dump
下面是运行结果
已取消闹钟1 [GC (Allocation Failure) 24103K->21319K(98304K), 0.0187832 secs] 已取消闹钟2 已取消闹钟3 [GC (Allocation Failure) 42289K->41792K(98304K), 0.0081251 secs] 已取消闹钟4 已取消闹钟5 [GC (Allocation Failure) 63024K->62160K(98304K), 0.0079021 secs] [Full GC (Ergonomics) 62160K->62038K(98304K), 0.0261820 secs] 已取消闹钟6 已取消闹钟7 [Full GC (Ergonomics) 83014K->82518K(98304K), 0.0083257 secs] [Full GC (Allocation Failure) 82518K->82503K(98304K), 0.0088677 secs] java.lang.OutOfMemoryError: Java heap space Dumping heap to d:/timer.dump ... Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at com.haanoo.schedule.ScheduleDemo$AlarmTask.<init>(ScheduleDemo.java:25) at com.haanoo.schedule.ScheduleDemo.main(ScheduleDemo.java:15) [闹钟0]嘀。。。 Heap dump file created [85271860 bytes in 0.052 secs]
从日志能够看出GC一直在努力,中间进行了3次Full GC(此时会影响应用性能),但基本没啥效果。
再用MAT看一下堆快照
经过MAT观察则一目了然,失效的7个闹钟(每一个10M)占据了70M堆内存。
经过上面的分析能够看到,虽然TimeTask.cancel()提供了一个及时取消的接口,但却没有一个自动机制保证失效的任务及时回收(须要用户手动处理)。
解决方法
为了防止内存泄漏,Timer提供了一个接口purge()及时清除无效任务。
public class Timer { private final TaskQueue queue = new TaskQueue(); public int purge() { int result = 0; synchronized(queue) { for (int i = queue.size(); i > 0; i--) { if (queue.get(i).state == TimerTask.CANCELLED) { // 清除无效任务 queue.quickRemove(i); result++; } } if (result != 0) // 从新整理队列中得任务 queue.heapify(); } return result; }
用户只要合理地使用timer.purge()就能避免内存泄漏,遗憾地是在我所接触的项目中,(或许没有引发重视)基本没有用到这个接口方法。