原文地址: 有個定時任務忽然不執行了,別急,緣由可能在這轉載請註明出處!java
小夥伴們,咱們一塊兒來避坑😅😅
程序發版以後一個定時任務忽然掛了!this
「幸好是用灰度跑的,否則完蛋了。😭」spa
以前由於在線程池踩過坑,閱讀過ThreadPoolExecutor
的源碼,自覺得不會再踩坑,沒想到又一不當心踩坑了,只不過此次的坑踩在了ScheduledThreadPoolExecutor
上面。寫代碼真的是要注意細節上的東西。線程
ScheduledThreadPoolExecutor
是ThreadPoolExecutor
功能的延伸(繼承關係),按照之前的經驗,很快就知道的問題所在,特此記錄一下。但願小夥伴們別重蹈覆轍。3d
代碼模擬:rest
public class ScheduledExecutorTest { private static LongAdder longAdder = new LongAdder(); public static void main(String[] args) { ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor(); scheduledExecutor.scheduleAtFixedRate(ThreadExecutorExample::doTask, 1, 1, TimeUnit.SECONDS); } private static void doTask() { int count = longAdder.intValue(); longAdder.increment(); System.out.println("定時任務開始執行 === " + count); // ① 下面這一段註釋前和註釋後的區別 if (count == 3) { throw new RuntimeException("some runtime exception"); } } }
代碼塊①註釋的狀況下,執行結果:日誌
定時任務開始執行 === 0 定時任務開始執行 === 1 定時任務開始執行 === 2 定時任務開始執行 === 3 定時任務開始執行 === 4 定時任務開始執行 === 5 定時任務開始執行 === 6 定時任務開始執行 === 7 定時任務開始執行 === 8 .... 會一直執行下去
代碼塊①不註釋的狀況下,執行結果:code
定時任務開始執行 === 0 定時任務開始執行 === 1 定時任務開始執行 === 2 定時任務開始執行 === 3 // 中止輸出,任務再也不被執行
由於任務最外面沒有用try-catch
捕捉,或者說任務執行時,遇到了 Uncaught Exception,因此致使這個定時任務中止執行了。對象
有了初步的結論,咱們須要知道的就是,ScheduledExecutorService
這個定時線程調度器(定時任務線程池)在碰到 Uncaught Exception 的時候,是怎麼處理的,是在哪一塊致使任務中止的?blog
以前是看過ThreadPoolExecutor
的源碼,當線程池的線程工做時拋出 Uncaught Exception 時,會這個線程拋棄掉,而後再新啓一個worker,來執行任務。在這裏顯然不同,由於這個問題的主體是定時任務,定時任務的後續執行中止了,而不是worker線程。
帶着問題,咱們走進源碼去看更深層次的答案。
這裏說一句,本文不會成爲
ScheduledThreadPoolExecutor
的完整源碼解析,只是在具體問題場景下,討論源碼的運行。
ScheduledExecutorService scheduledExecutor = Executors.newSingleThreadScheduledExecutor();
先看生成的ScheduledExecutorService
實例,
public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService (new ScheduledThreadPoolExecutor(1)); }
返回了一個DelegatedScheduledExecutorService
對象,
static class DelegatedScheduledExecutorService extends DelegatedExecutorService implements ScheduledExecutorService { private final ScheduledExecutorService e; DelegatedScheduledExecutorService(ScheduledExecutorService executor) { super(executor); e = executor; } public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) { return e.schedule(command, delay, unit); } public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) { return e.schedule(callable, delay, unit); } public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { return e.scheduleAtFixedRate(command, initialDelay, period, unit); } public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) { return e.scheduleWithFixedDelay(command, initialDelay, delay, unit); } }
發現這個類實際上就是把ScheduledExecutorService
包裝了一層,實際上的動做是由ScheduledThreadPoolExecutor
類執行的。
因此咱們再進去看,這裏咱們關注的scheduleAtFixedRate(...)
方法,也就是計劃執行定時任務的方法。
咱們先不急着看方法的實現,先看下它的接口層ScheduledExecutorService
,這個方法的 JavaDoc 上面寫了這麼一段話:
If any execution of the task encounters an exception, subsequent executions are suppressed.
Otherwise, the task will only terminate via cancellation or termination of the executor. If any execution of this task takes longer than its period, then subsequent executions may start late, but will not concurrently execute.
<font color = 'red'>若是任務的任何一次執行遇到異常,則將禁止後續執行</font>。其餘狀況下,任務將僅經過取消操做或終止線程池來中止。
若是某一次的執行時間超過了任務的間隔時間,後續任務會等當前此次執行結束才執行。
這個方法的註釋,已經告訴咱們了在使用這個方法的時候,要注意的事項了。
這裏說一句,線程池的使用中,註釋真的十分關鍵,把坑說的很清楚。(mdzz,說了那麼多你本身還不是沒看😓😓)
這個註釋已經解釋了一大半,可是咱們這個是源碼解析,固然看看裏面是怎麼作的,
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) { if (command == null || unit == null) throw new NullPointerException(); if (period <= 0) throw new IllegalArgumentException(); // ① ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; delayedExecute(t); return t; } protected <V> RunnableScheduledFuture<V> decorateTask( Runnable runnable, RunnableScheduledFuture<V> task) { return task; } private void delayedExecute(RunnableScheduledFuture<?> task) { if (isShutdown()) reject(task); else { super.getQueue().add(task); if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) task.cancel(false); else ensurePrestart(); } }
這裏的核心邏輯就是將 Runnable
包裝成了一個ScheduledFutureTask
對象,這個包裝是在FutureTask
基礎上增長了定時調度須要的一些數據。(FutureTask
是線程池的核心類之一)
decorateTask
是一個鉤子方法,用來給擴展用的,在這裏的默認實現就是返回ScheduledFutureTask
自己。
而後主邏輯就是經過delayedExecute
放入隊列中。(這裏省略對源碼中線程池shutdown狀況處理的解釋)
這裏咱們放一張圖,簡單描述一下ScheduledThreadPoolExecutor
工做的過程:
咱們很容易都推斷出來,咱們想要找的對於 Uncaught Exception 邏輯的處理確定是在任務執行的時候,從哪裏能夠看出來呢,就是ScheduledFutureTask
的run
方法。
public void run() { // 是不是週期性任務 boolean periodic = isPeriodic(); // 若是不能夠在當前狀態下運行,就取消任務(將這個任務的狀態設置爲CANCELLED)。 if (!canRunInCurrentRunState(periodic)) cancel(false); else if (!periodic) // 若是不是週期性的任務,調用 FutureTask # run 方法 ScheduledFutureTask.super.run(); else if (ScheduledFutureTask.super.runAndReset()) { // 若是是週期性的。 // 執行任務,但不設置返回值,成功後返回 true。 // 設置下次執行時間 setNextRunTime(); // 再次將任務添加到隊列中 reExecutePeriodic(outerTask); } }
這裏咱們關注的是ScheduledFutureTask.super.runAndReset()
,實際上調用的是其父類FutureTask
的
runAndReset()
方法,這個方法會在執行成功以後重置線程狀態,reset就是這個語義。
能夠看到,當上述方法執行返回false的時候,就不會再次將任務添加的隊列中,這和咱們最開始看到的異常狀況是一致的,看來答案就在這個方法裏面。那咱們接下去看看。
protected boolean runAndReset() { if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return false; boolean ran = false; int s = state; try { Callable<V> c = callable; if (c != null && s == NEW) { try { // ① 任務執行 c.call(); // don't set result ran = true; } catch (Throwable ex) { setException(ex); } } } finally { // runner must be non-null until state is settled to // prevent concurrent calls to run() runner = null; // state must be re-read after nulling runner to prevent // leaked interrupts s = state; if (s >= INTERRUPTING) handlePossibleCancellationInterrupt(s); } // return ran && s == NEW; }
代碼塊①是執行任務的地方,這裏有一個默認爲false的ran
變量,當任務執行成功時,ran
會被設成 true,即任務已執行。能夠看到當代碼塊①拋出異常的時候,ran
等於false,runAndReset()
返回給調用方的最終結果是false,也就應驗了咱們上面說的邏輯走向。
整篇文章到這裏結束啦,本篇主要介紹了當ScheduledThreadPoolExecutor
碰到 Uncaught Exception 時的源碼處理邏輯。咱們本身在使用這個線程池時,須要注意對任務運行時異常的處理(最簡單的方式就是在最外層加個try-catch
,而後捕捉打印日誌)。
若是本文有幫助到你,但願能點個贊,這是對個人最大動力🤝🤝🤗🤗。