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()就能避免內存泄漏,遺憾地是在我所接觸的項目中,(或許沒有引發重視)基本沒有用到這個接口方法。