Timer與ScheduledThreadPoolExecutor

在實際應用中,有時候我們需要創建一些個延遲的、並具有週期性的任務,比如,我們希望當我們的程序啓動後每隔1小時就去做一次日誌記錄。在JDK中提供了兩種方法去創建延遲週期性任務。


Timer
Timer是java.util包下的一個類,在JDK1.3的時候被引入,Timer只是充當了一個執行者的角色,真正的任務邏輯是通過一個叫做TimerTask的抽象類完成的,TimerTask也是java.util包下面的類,它是一個實現了Runnable接口的抽象類,包含一個抽象方法run( )方法,需要我們自己去提供具體的業務實現。
Timer類對象是通過其schedule方法執行TimerTask對象中定義的業務邏輯,並且schedule方法擁有多個重載方法提供不同的延遲與週期性服務。

下面是利用Timer去創建的一個延時週期性任務
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TestTimer {

	public static void main(String[] args) {
		
		String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
		System.out.println("Start time : " + time);
		
		Timer timer = new Timer();
		TimerTask task = new TimerTask() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
				System.out.println("Now Time : "  + time);
			}
		}; //end task
		
		timer.schedule(task, 2000, 3000);
		
	}
	
}

程序的輸出:
Start time : 21:36:08
Now Time : 21:36:10
Now Time : 21:36:13
Now Time : 21:36:16
Now Time : 21:36:19

ScheduledThreadPoolExecutor
在JDK1.5的時候在java.util.concurrent併發包下引入了ScheduledThreadPoolExecutor類,引入它的原因是因爲Timer類創建的延遲週期性任務存在一些缺陷,ScheduledThreadPoolExecutor繼承了ThreadPoolExecutor,並且實現了ScheduledExecutorService接口,ScheduledThreadPoolExecutor也是通過schedule方法執行Runnable任務的。
我們用ScheduledThreadPoolExecutor來實現和上述Timer一樣的功能
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class TestScheduledThreadPoolExecutor {

	public static void main(String[] args) {
		
		String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
		System.out.println("Start time : " + time);
		
		ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(5);  //創建5個執行線程
		
		Runnable runnable = new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
				System.out.println("Now Time : "  + time);
			}
		};
		
		executor.scheduleWithFixedDelay(runnable, 2, 3, TimeUnit.SECONDS);
		
	}
	
}

程序的輸出:
Start time : 22:12:25
Now Time : 22:12:27
Now Time : 22:12:30
Now Time : 22:12:33
Now Time : 22:12:36

這樣看來Timer和ScheduledThreadPoolExecutor好像沒有聲明差別,但是ScheduledThreadPoolExecutor的引入正是由於Timer類存在的一些不足,並且在JDK1.5或更高版本中,幾乎沒有利用繼續使用Timer類,下面說明Timer存在的一些缺點。

單線程
Timer類是通過單線程來執行所有的TimerTask任務的,如果一個任務的執行過程非常耗時,將會導致其他任務的時效性出現問題。而ScheduledThreadPoolExecutor是基於線程池的多線程執行任務,不會存在這樣的問題。
這裏我們通過讓Timer來執行兩個TimerTask任務來說明,其中一個TimerTask的執行過程是耗時的,加入需要2秒。
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class SingleThreadTimer {

	public static void main(String[] args) {
		
		String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
		System.out.println("Start time : " + time);
		
		Timer timer = new Timer();
		
		TimerTask task1 = new TimerTask() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
				System.out.println("Task1 time : " + time);
			}
		};
		
		TimerTask task2 = new TimerTask() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				String time = new SimpleDateFormat("HH:mm:ss").format(new Date());
				System.out.println("task2 time : " + time);
			}
		};
		
		timer.schedule(task1, 2000, 1000);
		timer.schedule(task2, 2000, 3000);
	}
	
}

這裏定義了兩個任務,任務1,程序啓動2秒後每隔1秒運行一次,任務2,程序啓動2秒後,每隔3秒運行1次,然後讓Timer同時運行這兩個任務
程序的輸出如下:
Start time : 22:22:37
Task1 time : 22:22:39
task2 time : 22:22:41
Task1 time : 22:22:41
Task1 time : 22:22:42
task2 time : 22:22:44
Task1 time : 22:22:44
Task1 time : 22:22:45
task2 time : 22:22:47
Task1 time : 22:22:47
Task1 time : 22:22:48

可以分析,無論是任務1還是任務2都沒有按照我們設定的預期進行運行,造成這個現象的原因就是Timer類是單線程的。

Timer線程不捕獲異常
Timer類中是不捕獲異常的,假如一個TimerTask中拋出未檢查異常(P.S:java中異常分爲兩類:checked exception(檢查異常)和unchecked exception(未檢查異常),對於未檢查異常也叫RuntimeException(運行時異常). ),Timer類將不會處理這個異常而產生無法預料的錯誤。這樣一個任務拋出異常將會導致整個Timer中的任務都被取消,此時已安排但未執行的TimerTask也永遠不會執行了,新的任務也不能被調度(所謂的“線程泄漏”現象)。
下面就已常見的RuntimeException,ArrayIndexOutOfBoundsException數組越界異常,來演示這個缺點:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class TestTimerTask {

	public static void main(String[] args) {
		System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
		Timer timer = new Timer();
		TimerTask task1 = new TimerTask() {
		
			@Override
			public void run() {
				System.out.println("1: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
			}
		};
		
		TimerTask task2 = new TimerTask() {
			
			@Override
			public void run() {
				int[] arr = {1,2,3,4,5};
				try {
					Thread.sleep(1000);
					
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				int index = (int)(Math.random()*100);
				System.out.println(arr[index]);
				
				System.out.println("2: " + new SimpleDateFormat("HH:mm:ss").format(new Date()));
			}
		};
		
		timer.schedule(task1, 2000, 3000);
		timer.schedule(task2, 2000, 1000);
		
	}
	
}

程序會在運行過程中拋出數組越界異常,並且整個程序都會被終止,原來完好的任務1也被終止了。

基於絕對時間
Timer類的調度是基於絕對的時間的,而不是相對的時間,因此Timer類對系統時鐘的變化是敏感的,舉個例子,加入你希望任務1每個10秒執行一次,某個時刻,你將系統時間提前了6秒,那麼任務1就會在4秒後執行,而不是10秒後。在ScheduledThreadPoolExecutor,任務的調度是基於相對時間的,原因是它在任務的內部存儲了該任務距離下次調度還需要的時間(使用的是基於System#nanoTime實現的相對時間,不會因爲系統時間改變而改變,如距離下次執行還有10秒,不會因爲將系統時間調前6秒而變成4秒後執行)。

基於以上3個弊端,在JDK1.5或以上版本中,我們幾乎沒有理由繼續使用Timer類,ScheduledThreadPoolExecutor可以很好的去替代Timer類來完成延遲週期性任務。







發佈了65 篇原創文章 · 獲贊 34 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章