Java併發學習之定時任務的幾種玩法

Java中建立和玩轉定時任務

定時任務,在平常工做中,能夠說是一個算是一個常見的需求場景,好比定時數據校驗,數據報表輸出,報警等java

0. 前言

前面一篇博文《Java併發學習之四種線程建立方式的實現與對比》, 有朋友指出線程池的方式應該算不上新的方式,而應該把Timer方式建立線程加上spring

這個倒是我我的見識不夠,寫的時候沒有想到Timer這種場景了,因此說分享學習記錄,不只僅能夠幫助別人,本身也會所以收益併發

感謝@超大小龍蝦 的指正,同時歡迎各位大俠對小弟多多指教框架

I. 定時任務建立的幾種方式

這裏給出幾種我的接觸過的定時任務使用方式(不全,僅供你們參考)異步

  1. 最簡單的一種:在線程中執行 Thread.sleep(),休眠掛起線程,等待一段時間後再執行
  2. 藉助Timer和TimerTask實現定時任務
  3. 藉助調度線程池 Executors.newScheduledThreadPool() 實現定時任務
  4. 藉助第三方工具,如spring的定時任務; Quartz(聽過沒用過);以及其餘一些開源工具或公司內的服務

下面簡單介紹上面的幾種思路,以及通常的使用姿式ide

1. Thread#sleep方式

嚴格來說,這種不太可以算入定時任務的範疇,爲何這麼說?工具

通常咱們所說的定時任務能夠區分爲兩種,一種是到了某個點自動執行;另外一種就是每隔多長時間執行一次學習

而這種線程Sleep的方式,則是在運行後,強制使線程進入阻塞狀態一段時間,而後再執行後續的邏輯,通常的使用流程是.net

// 提早的業務邏輯 xxx
try {
  Thread.sleep(1000); // 睡眠1s
} catch(Exception e) {
  // ....
}
// 定時任務的業務邏輯
// xxx

這裏把這個也放在定時任務裏,能夠看下面結合實例的case中的演示,利用這個sleep也能夠很是猥瑣的實現定時需求線程

2. Timer & TimerTask方式

TimerTask 是一個實現 Runnable的抽象類,所以能夠將須要定時處理的業務邏輯封裝在這個Task裏面;而後經過Timer封裝類來定時調度

TimerTask的使用姿式和通常的Runnable接口沒啥兩樣

通常使用姿式以下

// 建立timer實例
Timer timer = new Timer("demo);

// 撰寫定時任務邏輯
TimerTask task = new TimerTask() {
    @Override
    public void run() {
        System.out.println("timerTask: " + System.currentTimeMillis());
    }
};

// 定時調度執行
// 1. 100ms後開始執行task任務
timer.schedule(task, 100); 

// 2. 100ms後,首次執行,而且每隔100ms執行一次task任務
timer.scheduleAtFixedRate(task, 100, 100);

這個就有意思一點了,能夠支持定時執行,也能夠支持按一個頻率執行,且通常使用能夠將上面的步驟進行縮減, 直接這麼玩

new Timer("schedule").schedule(new TimerTask() {
    @Override
    public void run() {
        System.out.println("timerTask: " + System.currentTimeMillis());
    }
}, 100);

3. Executors#newScheduledThreadPool線程池方式

Executors提供了一批建立線程池的方式,除了常見的建立固定大小的線程池以外,還有個一就是建立ScheduledExecutorService來實現定時任務調度

藉助Executors#newScheduledThreadPool來實現定時任務很是簡單

// 獲取線程池
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1);


// 延遲100ms後,執行定時任務
executorService.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("task: " + System.currentTimeMillis());
    }
}, 100, TimeUnit.MILLISECONDS);


// 100ms後,首次執行,而後每一個100ms執行一次
executorService.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("task: " + System.currentTimeMillis());
    }
}, 100, 100, TimeUnit.MILLISECONDS)

從使用姿式來看,和Timer方式差不離,一樣支持定時執行與每隔多長時間執行兩種方式

4. spring的定時任務

spring方式就很是強大了,並且支持註解的配置方式,配置完畢,而後在方法上加一個註解,就能夠實現定時執行了

常見的使用姿式

// 100ms後執行
@Scheduled(fixedDelay = 100)
public void doSomething() { 
    // something that should execute periodically
}


// 每隔100ms執行一次
@Scheduled(fixedRate = 100)
public void doSomething() { 
    // something that should execute periodically
}

並且比較厲害的是,這個還支持cron表達式

II. 結合實例演示四種定時任務使用姿式

來兩個實際的應用場景,用上面的四種方式分別實現

case:

系統中有一些統計數據,須要離線計算,天天凌晨計算完以後導入,而後須要一個定時任務,假設凌晨5點數據導入完畢;在5:15分進行校驗,判斷數據是否正常導入;校驗完成以後,45分鐘後即六點,將校驗結果通知給owner

1. Thread#sleep 實現方式

採用sleep的方式實現定時任務,由於其自己不支持定時的狀況,因此就只能比較猥瑣的計算須要sleep的時間了

實現代碼以下(非精確的實現方式,主要爲了演示如何用sleep來實現上面這種場景)

public class SleepDemo {
  static class Task implements Runnable {
      public void run() {
          while (true) {
              Calendar calendar = Calendar.getInstance();
              int hour = calendar.get(Calendar.HOUR_OF_DAY);
              int min = calendar.get(Calendar.MINUTE);
              int sleepHour, sleepMin;

              // 計算sleep的小時數; 若在啓動時,在五點前 5-hour; 不然須要加一天
              sleepHour = 5 - hour < 0 ? (24 + 5 - hour) : (5 - hour);
              sleepMin = 15 - min; // 計算sleep的分鐘數
              try {
                  long sleepTime = ((sleepHour * 60) + sleepMin) * 60 * 1000L;
                  Thread.sleep(sleepTime);

                  // 開始校驗數據是否存在
                  System.out.println("數據校驗");

                  // 等待到6點,開始報警
                  int second = calendar.get(Calendar.SECOND);
                  sleepTime = ((59 - calendar.get(Calendar.MINUTE)) * 60 + 60 - second) * 1000L;
                  Thread.sleep(sleepTime);
                  
                  System.out.println("開始報警");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }

  public static void main(String[] args) {
      new Thread(new Task(),"sleepDemo").start();
  }
}

簡單說明下上面的實現思路:

  • 採用while(true)死循環,來實現每隔多長時間來執行一次
  • 對於定時觸發任務的場景,須要計算指定時間與當前時間的差值,做爲sleep的時間

這個實現方式,雖然說能夠完成目標,可是很是的不優雅,下面來看下Timer的實現方式

2. Timer&TimerTask 實現方式

使用Timer,須要藉助TimerTask類,在其中書寫定時任務的邏輯,由於case中有一個每隔一天跑一次的定時任務和一個延遲任務,因此這裏用到了Timer的兩種定時任務使用方式

public class TimerDemo {
    static class Task extends TimerTask {
        @Override
        public void run() {
            System.out.println("開始執行任務");

            // 執行完畢,等待到6點發送報警
            int min = Calendar.getInstance().get(Calendar.MINUTE);
            int sec = Calendar.getInstance().get(Calendar.SECOND);
            long delayTime = ((59 - min) * 60 + 60 - sec) * 1000L;
            new Timer().schedule(new TimerTask() {
                @Override
                public void run() {
                    System.out.println("報警");
                }
            }, delayTime);
        }
    }

    public static void main(String[] args) {
        Date date = new Date();
        if (date.getHours() == 5 && date.getMinutes() > 15 || date.getHours() > 5) {
            date.setHours(5);
        } else {
            date.setMinutes(15);
        }
        date.setSeconds(0);

        new Timer().scheduleAtFixedRate(new Task(), date, 24 * 2600 * 1000L);
    }
}

相比與上一個,稍微好了那麼一丟丟,至少從代碼結構上來看簡潔了不少

3. Executors.newScheduledThreadPool的實現方式

定時任務的方式,用起來和前面差很少,依然是兩種方式的混搭

public class ScheduleDemo {

    static ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);

    static class Task extends TimerTask {
        @Override
        public void run() {
            System.out.println("開始執行任務");

            // 執行完畢,等待到6點發送報警
            int min = Calendar.getInstance().get(Calendar.MINUTE);
            int sec = Calendar.getInstance().get(Calendar.SECOND);
            long delayTime = (59 - min) * 60 + 60 - sec;
            executorService.schedule(() -> System.out.println("報警"), delayTime, TimeUnit.SECONDS);
        }
    }

    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        // 計算sleep的小時數
        int sleepHour = 5 - calendar.get(Calendar.HOUR_OF_DAY);
        if(sleepHour < 0) { // 算下一天
            sleepHour = 24 + sleepHour;
        }

        // 計算sleep的分鐘數
        int sleepMin = 15 - calendar.get(Calendar.MINUTE);
        long sleepTime = ((sleepHour * 60) + sleepMin) * 60 * 1000L;
        
        executorService.scheduleAtFixedRate(new Task(), sleepTime, 24 * 3600, TimeUnit.SECONDS);
    }
}

與Timer在用法上不一樣的一個是這裏能夠指定延遲的時間單位;可是但願在指定的時間進行執行時,依然仍是得計算初始的延遲時間,和sleep使用方式中差很少

上面三中,是jdk自己就支持的定時任務的支持;總得來講,能實現你的需求場景,可是很差用,還得讓本身去計算delayTime/sleepTime;講道理,這對使用者而言,實在是不能更不友好了;

可是在另外一方面,若延遲時間比較容易確認的話;或者單純的使用每隔多長時間調度一次的話,TimerScheduledExecutorService兩種方式都還不錯

  • Timer 在指定時間執行任務相比較 ScheduledExecutorService 而言優雅一點
  • ScheduledExecutorService 則勝在使用起來簡潔,並且schedule方法能夠提交Callable任務,並獲取返回值
  • Thread#sleep方法,則儘可能不要這麼玩,有點違和

4. 高逼格的Spring定時器

Spring 相比較jdk自帶的幾種方式而言,我認爲,最完美的有兩點

  • 支持cron表達式
  • 註解方式,無侵入

配置xml文件

<task:executor id="executor" pool-size="5" />
<task:scheduler id="scheduler" pool-size="10" />
<task:annotation-driven executor="executor" scheduler="scheduler" />

實現業務邏輯

@Component
public class ScheduleDemo {
    @Scheduled(cron = "0 0 5 * * ?")
    public void doSome() {
        System.out.println(" 校驗: " + System.currentTimeMillis());
    }

    
    @Scheduled(cron = "0 0 6 * * ?")
    public void alarm(){
        System.out.println(" 報警: " + System.currentTimeMillis());
    }
}

這個實現就簡單了,相比較上面而言,添加一個註解,裏面配置cron表達式,xml配置下,就能夠實現定時任務

III. 小結

1. 本片博文主要介紹了實現定時任務的方式有幾種,下面簡單小結下四種方式的特色

方式 說明 特色
Thread#sleep 線程掛起一段時間 經過定時目標與當前時間計算sleepTime,來強制實現定時任務
Timer#TimerTask 異步定時任務 TimerTask內部實現定時任務邏輯 <br/>1. Timer可按頻率調度任務 <br/> 2. Timer也支持指定時間調度任務
ScheduledExecutorService 計劃任務線程池 1. 利用Executors#newScheduledThreadPool;建立線程池 <br/> 2. 建立線程任務實現定時任務邏輯 <br/> 3. 提交線程池執行,支持按頻率調度,支持延遲多長時間調度 <br/> 4. 支持獲取返回值
Spring Schedule spring提供的定時任務 支持cron表達式,使用簡單,很是簡單,超級簡單

2. 使用Timer方式,也能夠算一種新的建立線程方式,

3. 使用小建議

不推薦使用 Thread#sleep的方式作定時任務

如指向利用jdk實現定時任務,能夠考慮 TimerScheduledExecutorService

如項目自己就利用到了Spring,能夠優先考慮這些優秀的框架提供的服務,用起來特別爽,誰用誰知道

IV. 其餘

聲明

盡信書則不如,已上內容,純屬一家之言,因本人能力通常,見識有限,若有問題,請不吝指正,感激

掃描關注,java分享

QrCode

相關文章
相關標籤/搜索