1. 程式人生 > >【小家java】Java定時任務ScheduledThreadPoolExecutor詳解以及與Timer、TimerTask的區別

【小家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,所有任務都儲存在一個佇列中順序執行

,如果需要多個TimerTask併發執行,則需要建立兩個多個Timer。

很顯然,一個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執行緒的缺點(這個就重要了)

  1. Timer執行緒不會捕獲異常,所以TimerTask丟擲的未檢查的異常會終止timer執行緒。如果Timer執行緒中存在多個計劃任務,其中一個計劃任務丟擲未檢查的異常,則會引起整個Timer執行緒結束,從而導致其他計劃任務無法得到繼續執行。
  2. Timer執行緒時基於絕對時間(如:2014/02/14 16:06:00),因此計劃任務對系統的時間的改變是敏感的。(舉個例子,假如你希望任務1每個10秒執行一次,某個時刻,你將系統時間提前了6秒,那麼任務1就會在4秒後執行,而不是10秒後)
  3. Timer是單執行緒,如果某個任務很耗時,可能會影響其他計劃任務的執行。
  4. 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的使用有三點需要說明

  1. ScheduledThreadPoolExecutor繼承自ThreadPoolExecutor(ThreadPoolExecutor詳解),因而也有繼承而來的execute()和submit()方法,但是ScheduledThreadPoolExecutor重寫了這兩個方法,重寫的方式是直接建立兩個立即執行並且只執行一次的任務;
  2. ScheduledThreadPoolExecutor使用ScheduledFutureTask封裝每個需要執行的任務,而任務都是放入DelayedWorkQueue佇列中的,該佇列是一個使用陣列實現的優先佇列,在呼叫ScheduledFutureTask::cancel()方法時,其會根據removeOnCancel變數的設定來確認是否需要將當前任務真正的從佇列中移除,而不只是標識其為已刪除狀態;
  3. ScheduledThreadPoolExecutor提供了一個鉤子方法decorateTask(Runnable, RunnableScheduledFuture)用於對執行的任務進行裝飾,該方法第一個引數是呼叫方傳入的任務例項,第二個引數則是使用ScheduledFutureTask對使用者傳入任務例項進行封裝之後的例項。這裡需要注意的是,在ScheduledFutureTask物件中有一個heapIndex變數,該變數用於記錄當前例項處於佇列陣列中的下標位置,該變數可以將諸如contains(),remove()等方法的時間複雜度從O(N)降低到O(logN),因而效率提升是比較高的,但是如果這裡使用者重寫decorateTask()方法封裝了佇列中的任務例項,那麼heapIndex的優化就不存在了,因而這裡強烈建議是儘量不要重寫該方法,或者重寫時也還是複用ScheduledFutureTask類。