有個定時任務突然不執行了,別急,原因可能在這

點擊上方「藍字」關注我們

問題描述

程序發版之後一個定時任務突然掛了!

“幸虧是用灰度跑的,不然完蛋了。????”

之前因爲在線程池踩過坑,閱讀過ThreadPoolExecutor的源碼,自以爲不會再踩坑,沒想到又一不小心踩坑了,只不過這次的坑踩在了ScheduledThreadPoolExecutor上面。寫代碼真的是要注意細節上的東西。

ScheduledThreadPoolExecutorThreadPoolExecutor功能的延伸(繼承關係),按照以前的經驗,很快就知道的問題所在,特此記錄一下。希望小夥伴們別重蹈覆轍。

問題重現

代碼模擬:

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
.... 會一直執行下去

代碼塊①不註釋的情況下,執行結果:

定時任務開始執行 === 0
定時任務開始執行 === 1
定時任務開始執行 === 2
定時任務開始執行 === 3
// 停止輸出,任務不再被執行

初步結論

因爲任務最外面沒有用try-catch 捕捉,或者說任務執行時,遇到了 Uncaught Exception,所以導致這個定時任務停止執行了。

走進源碼看問題

有了初步的結論,我們需要知道的就是,ScheduledExecutorService這個定時線程調度器(定時任務線程池)在碰到 Uncaught Exception 的時候,是怎麼處理的,是在哪一塊導致任務停止的?

之前是看過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.

如果任務的任何一次執行遇到異常,則將禁止後續執行。其他情況下,任務將僅通過取消操作或終止線程池來停止。

如果某一次的執行時間超過了任務的間隔時間,後續任務會等當前這次執行結束才執行。

這個方法的註釋,已經告訴我們了在使用這個方法的時候,要注意的事項了。

  1. 要注意發生異常時,任務終止的情況。

  2. 要注意定時任務調度會等待正在執行的任務結束,纔會發起下一輪調度,即使超過了間隔時間。

這裏說一句,線程池的使用中,註釋真的十分關鍵,把坑說的很清楚。(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 邏輯的處理肯定是在任務執行的時候,從哪裏可以看出來呢,就是ScheduledFutureTaskrun方法。

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 ,然後捕捉打印日誌)。

有你想看的精彩 

LOL釣魚網站實戰滲透
邊緣計算  一文簡單讀懂
Tomcat是如何運行的?整體架構又是怎樣的?

支持百億級別的 Java 分佈式日誌組件EasyLog

戳這兒

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章