【小家java】Java定時任務ScheduledThreadPoolExecutor詳解以及與Timer、TimerTask的區別
相關閱讀
【小家java】java5新特性(簡述十大新特性) 重要一躍
【小家java】java6新特性(簡述十大新特性) 雞肋升級
【小家java】java7新特性(簡述八大新特性) 不溫不火
【小家java】java8新特性(簡述十大新特性) 飽受讚譽
【小家java】java9新特性(簡述十大新特性) 褒貶不一
【小家java】java10新特性(簡述十大新特性) 小步迭代
【小家java】java11新特性(簡述八大新特性) 首個重磅LTS版本
【小家java】Java中的執行緒池,你真的用對了嗎?(教你用正確的姿勢使用執行緒池)
小家Java】一次Java執行緒池誤用(newFixedThreadPool)引發的線上血案和總結
【小家java】BlockingQueue阻塞佇列詳解以及5大實現(ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue…)
【小家java】用 ThreadPoolExecutor/ThreadPoolTaskExecutor 執行緒池技術提高系統吞吐量(附帶執行緒池引數詳解和使用注意事項)
定時任務就是在指定時間執行程式,或週期性執行計劃任務。Java中實現定時任務的方法有很多,本文從從JDK自帶的一些方法來實現定時任務的需求。
Timer和TimerTask
本文先介紹Java最原始的解決方案:Timer和TimerTask
Timer和TimerTask可以作為執行緒實現的第三種方式,在JDK1.3的時候推出。但是自從JDK1.5之後不再推薦時間,而是使用ScheduledThreadPoolExecutor代替
public class Timer {}
// TimerTask 是個抽象類
public abstract class TimerTask implements Runnable {}
快速入門
Timer執行在後臺,可以執行任務一次,或定期執行任務。TimerTask類繼承了Runnable介面,因此具備多執行緒的能力。一個Timer可以排程任意多個TimerTask,所有任務都儲存在一個佇列中順序執行
很顯然,一個Timer定時器,是單執行緒的
public static void main(String[] args) throws ParseException {
Timer timer = new Timer();
//1、設定兩秒後執行任務
//timer.scheduleAtFixedRate(new MyTimerTask1(), 2000,1000);
//2、設定任務在執行時間執行,本例設定時間13:57:00
SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
Date time = dateFormatter.parse("2018/11/04 18:40:00");
//讓在指定的時刻執行(如果是過去時間會立馬執行 如果是將來時間 那就等吧)
timer.schedule(new MyTimerTask1(), time);
}
//被執行的任務必須繼承TimerTask,並且實現run方法
static class MyTimerTask1 extends TimerTask {
public void run() {
System.out.println("爆炸!!!");
}
}
相關API簡單介紹(畢竟已經不重要了):
schedule(TimerTask task, long delay, long period) --指定任務執行延遲時間
schedule(TimerTask task, Date time, long period) --指定任務執行時刻
scheduleAtFixedRate(TimerTask task, long delay, long period)
scheduleAtFixedRate(TimerTask task, Date firstTime, long period)
這裡需要注意區別:
- schedule:
- scheduleAtFixedRate:
相關文章度娘一下,可找到答案。因此本文不做介紹了,畢竟不是本文重點。
終止Timer執行緒
呼叫Timer.cancle()方法。可以在程式任何地方呼叫,甚至在TimerTask中的run方法中呼叫;
設定Timer物件為null,其會自動終止;
用System.exit方法,整個程式終止。
Timer執行緒的缺點(這個就重要了)
- Timer執行緒不會捕獲異常,所以TimerTask丟擲的未檢查的異常會終止timer執行緒。如果Timer執行緒中存在多個計劃任務,其中一個計劃任務丟擲未檢查的異常,則會引起整個Timer執行緒結束,從而導致其他計劃任務無法得到繼續執行。
- Timer執行緒時基於絕對時間(如:2014/02/14 16:06:00),因此計劃任務對系統的時間的改變是敏感的。(舉個例子,假如你希望任務1每個10秒執行一次,某個時刻,你將系統時間提前了6秒,那麼任務1就會在4秒後執行,而不是10秒後)
- Timer是單執行緒,如果某個任務很耗時,可能會影響其他計劃任務的執行。
- Timer執行程式是有可能延遲1、2毫秒,如果是1秒執行一次的任務,1分鐘有可能延遲60毫秒,一小時延遲3600毫秒,相當於3秒(如果你的任務對時間敏感,這將會有影響) ScheduledThreadPoolExecutor的時間會更加的精確
ScheduledThreadPoolExecutor解決了上述所有問題~
ScheduledThreadPoolExecutor(JDK全新定時器排程)
ScheduledThreadPoolExecutor是JDK1.5以後推出的類,用於實現定時、重複執行的功能,官方文件解釋要優於Timer。
構造方法:
ScheduledThreadPoolExecutor(int corePoolSize) //使用給定核心池大小建立一個新定定時執行緒池
ScheduledThreadPoolExecutor(int corePoolSize, ThreadFactorythreadFactory) //使用給定的初始引數建立一個新物件,可提供執行緒建立工廠
需要手動傳入執行緒工廠的,可以這麼弄:
private final static ScheduledThreadPoolExecutor schedual = new ScheduledThreadPoolExecutor(1, new ThreadFactory() {
private AtomicInteger atoInteger = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setName("xxx-Thread " + atoInteger.getAndIncrement());
return t;
}
});
相關排程方法:
ScheduledThreadPoolExecutor還提供了非常靈活的API,用於執行任務。其任務的執行策略主要分為兩大類:
①在一定延遲之後只執行一次某個任務;
②在一定延遲之後週期性的執行某個任務;
如下是其主要API:
// 執行一次
public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
// 週期性執行
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay, long delay, TimeUnit unit);
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay, long period, TimeUnit unit);
第一個和第二個方法屬於第一類,即在delay指定的延遲之後執行第一個引數所指定的任務,區別在於,第二個方法執行之後會有返回值,而第一個方法執行之後是沒有返回值的。
第三個和第四個方法則屬於第二類,即在第二個引數(initialDelay)指定的時間之後開始週期性的執行任務,執行週期間隔為第三個引數指定的時間。
但是這兩個方法的區別在於:第三個方法執行任務的間隔是固定的,無論上一個任務是否執行完成(也就是前面的任務執行慢不會影響我後面的執行
)。而第四個方法的執行時間間隔是不固定的,其會在週期任務的上一個任務執行完成之後才開始計時,並在指定時間間隔之後才開始執行任務。
public class ScheduledThreadPoolExecutorTest {
private ScheduledThreadPoolExecutor executor;
private Runnable task;
@Before
public void before() {
executor = initExecutor();
task = initTask();
}
private ScheduledThreadPoolExecutor initExecutor() {
return new ScheduledThreadPoolExecutor(2);;
}
private Runnable initTask() {
long start = System.currentTimeMillis();
return () -> {
print("start task: " + getPeriod(start, System.currentTimeMillis()));
sleep(SECONDS, 10);
print("end task: " + getPeriod(start, System.currentTimeMillis()));
};
}
@Test
public void testFixedTask() {
print("start main thread");
executor.scheduleAtFixedRate(task, 15, 30, SECONDS);
sleep(SECONDS, 120);
print("end main thread");
}
@Test
public void testDelayedTask() {
print("start main thread");
executor.scheduleWithFixedDelay(task, 15, 30, SECONDS);
sleep(SECONDS, 120);
print("end main thread");
}
private void sleep(TimeUnit unit, long time) {
try {
unit.sleep(time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private int getPeriod(long start, long end) {
return (int)(end - start) / 1000;
}
private void print(String msg) {
System.out.println(msg);
}
}
第一個輸出:
start main thread
start task: 15
end task: 25
start task: 45
end task: 55
start task: 75
end task: 85
start task: 105
end task: 115
end main thread
第二個輸出:
start main thread
start task: 15
end task: 25
start task: 55
end task: 65
start task: 95
end task: 105
end main thread
從結果,現在重點說說這兩者的區別:
scheduleAtFixedRate
- 是以上一個任務開始的時間計時,period時間過去後,檢測上一個任務是否執行完畢,如果上一個任務執行完畢,則當前任務立即執行,如果上一個任務沒有執行完畢,則需要等上一個任務執行完畢後立即執行。
- 執行週期是 initialDelay 、initialDelay+period 、initialDelay + 2 * period} 、 … 如果延遲任務的執行時間大於了 period,比如為 5s,則後面的執行會等待5s才回去執行
scheduleWithFixedDelay
是以上一個任務結束時開始計時,period時間過去後,立即執行, 由上面的執行結果可以看出,第一個任務開始和第二個任務開始的間隔時間是 第一個任務的執行時間+period(永遠是這麼多)
注意: 通過ScheduledExecutorService執行的週期任務,如果任務執行過程中丟擲了異常,那麼過ScheduledExecutorService就會停止執行任務,且也不會再週期地執行該任務了。所以你如果想保住任務都一直被週期執行,那麼catch一切可能的異常。
關於ScheduledThreadPoolExecutor的使用有三點需要說明
- ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor(ThreadPoolExecutor詳解),因而也有繼承而來的execute()和submit()方法,但是ScheduledThreadPoolExecutor重寫了這兩個方法,重寫的方式是直接建立兩個立即執行並且只執行一次的任務;
- ScheduledThreadPoolExecutor使用ScheduledFutureTask封裝每個需要執行的任務,而任務都是放入DelayedWorkQueue佇列中的,該佇列是一個使用陣列實現的優先佇列,在呼叫ScheduledFutureTask::cancel()方法時,其會根據removeOnCancel變數的設定來確認是否需要將當前任務真正的從佇列中移除,而不只是標識其為已刪除狀態;
- ScheduledThreadPoolExecutor提供了一個
鉤子方法decorateTask(Runnable, RunnableScheduledFuture)
用於對執行的任務進行裝飾,該方法第一個引數是呼叫方傳入的任務例項,第二個引數則是使用ScheduledFutureTask對使用者傳入任務例項進行封裝之後的例項。這裡需要注意的是,在ScheduledFutureTask物件中有一個heapIndex變數,該變數用於記錄當前例項處於佇列陣列中的下標位置,該變數可以將諸如contains(),remove()等方法的時間複雜度從O(N)降低到O(logN),因而效率提升是比較高的,但是如果這裡使用者重寫decorateTask()方法封裝了佇列中的任務例項,那麼heapIndex的優化就不存在了,因而這裡強烈建議是儘量不要重寫該方法,或者重寫時也還是複用ScheduledFutureTask類。