前面介紹了普通線程池的用法,就大多數任務而言,它們對具體的執行時機並無特殊要求,最多是希望早點跑完早點出結果。不過對於需要定時執行的任務來說,它們要求在特定的時間點運行,並且往往不止運行一次,還要周期性地反復運行。由於普通線程池滿足不了此類定時運行的需求,因此Java又提供了定時器線程池來實現定時與周期執行任務的功能。
普通線程池的工具類名叫ExecutorService,定時器線程池的工具類則叫做ScheduledExecutorService,添加了Scheduled前綴,表示它是一種有計划的、預先安排好的線程池。有別於划分了四大類的普通線程池,定時器線程池僅僅分成了兩類:單線程的定時器線程池和固定數量的定時器線程池。其中單線程的定時器線程池通過newSingleThreadScheduledExecutor方法獲得,它的創建代碼示例如下:
// 創建一個延遲一次的單線程定時器 ScheduledExecutorService pool = (ScheduledExecutorService) Executors.newSingleThreadScheduledExecutor();
至於固定數量的定時器線程池則通過newScheduledThreadPool方法獲得,它的創建代碼示例如下:
// 創建一個延遲一次的多線程定時器(線程池大小為3) ScheduledExecutorService pool = (ScheduledExecutorService) Executors.newScheduledThreadPool(3);
雖然定時器線程池只有兩類,但定時器的調度方式有三種之多,主要是依據啟動次數與周期長度來划分,詳細說明如下:
1、定時任務只啟動一次。
此時調用線程池對象的schedule方法,該方法的第一個參數為任務實例,第二個和第三個參數分別是延遲執行的時長及其單位。
2、每間隔若干時間周期啟動定時任務。
此時調用線程池對象的scheduleAtFixedRate方法,該方法的第一個參數為任務實例,第二個參數為首次執行的延遲時長,第三個參數分別為后續運行的間隔時長,第四個參數則為時長單位。
3、固定延遲若干時間啟動定時任務。
此時調用線程池對象的scheduleWithFixedDelay方法,該方法的參數說明基本同scheduleAtFixedRate方法。兩個方法的區別在於:前者的間隔時間從上個任務的開始時間起計算,后者的間隔時間從上個任務的結束時間起計算。
除了以上的三個調度方法,ScheduledExecutorService還擁有ExecutorService的全部方法,包括getPoolSize、getActiveCount、shutdown等等,因為它本來就是從ExecutorService派生而來的呀。
下面做個實驗觀察一下兩種定時器線程池的運行過程,實驗開始前先定義一個參觀任務,主要用來打印當前的操作日志,包括操作時間、操作線程、操作描述等信息。參觀任務的代碼例子如下所示:
// 定義一個參觀任務 private static class Visit implements Runnable { private String name; // 任務名稱 private int index; // 任務序號 public Visit(String name, int index) { this.name = name; this.index = index; } @Override public void run() { // 以下打印操作日志,包括操作時間、操作線程、操作描述等信息 String desc = String.format("%s的第%d個任務到此一游", name, index); PrintUtils.print(Thread.currentThread().getName(), desc); } };
然后命令單線程的定時器線程池調用schedule方法執行一次的定時任務,具體的實驗代碼示例如下:
// 測試延遲一次的單線程定時器 private static void testSingleScheduleOnce() { // 創建一個延遲一次的單線程定時器 ScheduledExecutorService pool = (ScheduledExecutorService) Executors.newSingleThreadScheduledExecutor(); for (int i=0; i<5; i++) { // 循環開展5個調度 // 創建一個參觀任務 Visit visit = new Visit("延遲一次的單線程定時器", i); // 命令線程池開展任務調度。延遲1秒后執行參觀任務 pool.schedule(visit, 1, TimeUnit.SECONDS); } }
運行以上的實驗代碼,觀察到如下的線程池日志:
15:49:16.122 pool-1-thread-1 延遲一次的單線程定時器的第0個任務到此一游 15:49:16.123 pool-1-thread-1 延遲一次的單線程定時器的第1個任務到此一游 15:49:16.123 pool-1-thread-1 延遲一次的單線程定時器的第2個任務到此一游 15:49:16.124 pool-1-thread-1 延遲一次的單線程定時器的第3個任務到此一游 15:49:16.124 pool-1-thread-1 延遲一次的單線程定時器的第4個任務到此一游
由日志可見,該定時器線程池自始至終只有唯一一個的線程在運行。
再來測試固定數量的定時器線程池,此時換成調用scheduleAtFixedRate方法,准備以固定頻率周期性地執行定時任務,具體的實驗代碼示例如下:
// 測試固定速率的多線程定時器 private static void testMultiScheduleRate() { // 創建一個固定速率的多線程定時器(線程池大小為3) ScheduledExecutorService pool = (ScheduledExecutorService) Executors.newScheduledThreadPool(3); for (int i=0; i<5; i++) { // 循環開展5個調度 // 創建一個參觀任務 Visit visit = new Visit("固定速率的多線程定時器", i); // 命令線程池開展任務調度。第一次延遲1秒后執行參觀任務,以后每間隔3秒執行下一個參觀任務 pool.scheduleAtFixedRate(visit, 1, 3, TimeUnit.SECONDS); } }
運行以上的實驗代碼,觀察到如下的線程池日志:
15:50:21.859 pool-1-thread-1 固定速率的多線程定時器的第0個任務到此一游 15:50:21.859 pool-1-thread-2 固定速率的多線程定時器的第1個任務到此一游 15:50:21.859 pool-1-thread-3 固定速率的多線程定時器的第2個任務到此一游 15:50:21.860 pool-1-thread-3 固定速率的多線程定時器的第3個任務到此一游 15:50:21.861 pool-1-thread-3 固定速率的多線程定時器的第4個任務到此一游 15:50:24.790 pool-1-thread-3 固定速率的多線程定時器的第1個任務到此一游 15:50:24.791 pool-1-thread-3 固定速率的多線程定時器的第3個任務到此一游 15:50:24.792 pool-1-thread-3 固定速率的多線程定時器的第4個任務到此一游 15:50:24.793 pool-1-thread-2 固定速率的多線程定時器的第2個任務到此一游 15:50:24.798 pool-1-thread-1 固定速率的多線程定時器的第0個任務到此一游
由日志可見,該定時器線程池一共開啟了三個線程來執行定時任務,注意到每個任務的前后日志間隔時間不足3秒,正好說明間隔的3秒並非前后兩次運行的首尾間隔。
那么調用方法改成scheduleWithFixedDelay,試試以固定間隔周期性地執行定時任務會是什么樣的,具體的實驗代碼示例如下:
// 測試固定延遲的多線程定時器 private static void testMultiScheduleDelay() { // 創建一個固定速率的多線程定時器(線程池大小為3) ScheduledExecutorService pool = (ScheduledExecutorService) Executors.newScheduledThreadPool(3); for (int i=0; i<5; i++) { // 循環開展5個調度 // 創建一個參觀任務 Visit visit = new Visit("固定延遲的多線程定時器", i); // 命令線程池開展任務調度。第一次延遲1秒后執行參觀任務,以后每3秒執行下一個參觀任務 pool.scheduleWithFixedDelay(visit, 1, 3, TimeUnit.SECONDS); } }
運行以上的實驗代碼,觀察到如下的線程池日志:
16:10:19.281 pool-1-thread-1 固定延遲的多線程定時器的第0個任務到此一游 16:10:19.281 pool-1-thread-2 固定延遲的多線程定時器的第1個任務到此一游 16:10:19.281 pool-1-thread-3 固定延遲的多線程定時器的第2個任務到此一游 16:10:19.283 pool-1-thread-3 固定延遲的多線程定時器的第3個任務到此一游 16:10:19.283 pool-1-thread-2 固定延遲的多線程定時器的第4個任務到此一游 16:10:22.283 pool-1-thread-1 固定延遲的多線程定時器的第1個任務到此一游 16:10:22.284 pool-1-thread-2 固定延遲的多線程定時器的第3個任務到此一游 16:10:22.285 pool-1-thread-3 固定延遲的多線程定時器的第2個任務到此一游 16:10:22.286 pool-1-thread-3 固定延遲的多線程定時器的第4個任務到此一游 16:10:22.287 pool-1-thread-1 固定延遲的多線程定時器的第0個任務到此一游
由日志可見,此時每個任務的前后日志時間均不小於3秒,證明了scheduleWithFixedDelay方法的確采取了固定間隔而非固定速率。
更多Java技術文章參見《Java開發筆記(序)章節目錄》