詳解Java ScheduledThreadPoolExecutor的踩坑與解決方法
概述
最近項(xiàng)目上反饋某個(gè)重要的定時(shí)任務(wù)突然不執(zhí)行了,很頭疼,開發(fā)環(huán)境和測(cè)試環(huán)境都沒有出現(xiàn)過這個(gè)問題。定時(shí)任務(wù)采用的是ScheduledThreadPoolExecutor
,后來一看代碼發(fā)現(xiàn)踩了一個(gè)大坑....
還原"大坑"
這個(gè)坑就是如果ScheduledThreadPoolExecutor
中執(zhí)行的任務(wù)出錯(cuò)拋出異常后,不僅不會(huì)打印異常堆棧信息,同時(shí)還會(huì)取消后面的調(diào)度, 直接看例子。
@Test public void testException() throws InterruptedException { // 創(chuàng)建1個(gè)線程的調(diào)度任務(wù)線程池 ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); // 創(chuàng)建一個(gè)任務(wù) Runnable runnable = new Runnable() { volatile int num = 0; @Override public void run() { num ++; // 模擬執(zhí)行報(bào)錯(cuò) if(num > 5) { throw new RuntimeException("執(zhí)行錯(cuò)誤"); } log.info("exec num: [{}].....", num); } }; // 每隔1秒鐘執(zhí)行一次任務(wù) scheduledExecutorService.scheduleAtFixedRate(runnable, 0, 1, TimeUnit.SECONDS); Thread.sleep(10000); }
運(yùn)行結(jié)果:
- 只執(zhí)行了5次后,就不打印,不執(zhí)行了,因?yàn)閳?bào)錯(cuò)了
- 任務(wù)報(bào)錯(cuò),也沒有打印一次堆棧,更導(dǎo)致調(diào)度任務(wù)取消,后果十分嚴(yán)重。
解決方案
解決方法也非常簡(jiǎn)單,只要通過try catch捕獲異常即可。
運(yùn)行結(jié)果:
看到不僅打印了異常堆棧,而且也會(huì)進(jìn)行周期性的調(diào)度。
更推薦的做法
更好的建議可以在自己的項(xiàng)目中封裝一個(gè)包裝類,要求所有的調(diào)度都提交通過我們統(tǒng)一的包裝類, 如下代碼:
@Slf4j public class RunnableWrapper implements Runnable { // 實(shí)際要執(zhí)行的線程任務(wù) private Runnable task; // 線程任務(wù)被創(chuàng)建出來的時(shí)間 private long createTime; // 線程任務(wù)被線程池運(yùn)行的開始時(shí)間 private long startTime; // 線程任務(wù)被線程池運(yùn)行的結(jié)束時(shí)間 private long endTime; // 線程信息 private String taskInfo; private boolean showWaitLog; /** * 執(zhí)行間隔時(shí)間多久,打印日志 */ private long durMs = 1000L; // 當(dāng)這個(gè)任務(wù)被創(chuàng)建出來的時(shí)候,就會(huì)設(shè)置他的創(chuàng)建時(shí)間 // 但是接下來有可能這個(gè)任務(wù)提交到線程池后,會(huì)進(jìn)入線程池的隊(duì)列排隊(duì) public RunnableWrapper(Runnable task, String taskInfo) { this.task = task; this.taskInfo = taskInfo; this.createTime = System.currentTimeMillis(); } public void setShowWaitLog(boolean showWaitLog) { this.showWaitLog = showWaitLog; } public void setDurMs(long durMs) { this.durMs = durMs; } // 當(dāng)任務(wù)在線程池排隊(duì)的時(shí)候,這個(gè)run方法是不會(huì)被運(yùn)行的 // 但是當(dāng)任務(wù)結(jié)束了排隊(duì),得到線程池運(yùn)行機(jī)會(huì)的時(shí)候,這個(gè)方法會(huì)被調(diào)用 // 此時(shí)就可以設(shè)置線程任務(wù)的開始運(yùn)行時(shí)間 @Override public void run() { this.startTime = System.currentTimeMillis(); // 此處可以通過調(diào)用監(jiān)控系統(tǒng)的API,實(shí)現(xiàn)監(jiān)控指標(biāo)上報(bào) // 用線程任務(wù)的startTime-createTime,其實(shí)就是任務(wù)排隊(duì)時(shí)間 // 這邊打印日志輸出,也可以輸出到監(jiān)控系統(tǒng)中 if(showWaitLog) { log.info("任務(wù)信息: [{}], 任務(wù)排隊(duì)時(shí)間: [{}]ms", taskInfo, startTime - createTime); } // 接著可以調(diào)用包裝的實(shí)際任務(wù)的run方法 try { task.run(); } catch (Exception e) { log.error("run task error", e); throw e; } // 任務(wù)運(yùn)行完畢以后,會(huì)設(shè)置任務(wù)運(yùn)行結(jié)束的時(shí)間 this.endTime = System.currentTimeMillis(); // 此處可以通過調(diào)用監(jiān)控系統(tǒng)的API,實(shí)現(xiàn)監(jiān)控指標(biāo)上報(bào) // 用線程任務(wù)的endTime - startTime,其實(shí)就是任務(wù)運(yùn)行時(shí)間 // 這邊打印任務(wù)執(zhí)行時(shí)間,也可以輸出到監(jiān)控系統(tǒng)中 if(endTime - startTime > durMs) { log.info("任務(wù)信息: [{}], 任務(wù)執(zhí)行時(shí)間: [{}]ms", taskInfo, endTime - startTime); } } }
使用:
我們還可以在包裝類里面封裝各種監(jiān)控行為,如本例打印日志執(zhí)行時(shí)間等。
原理探究
那大家有沒有想過為什么任務(wù)出錯(cuò)會(huì)導(dǎo)致異常無法打印,甚至調(diào)度都取消了呢?讓我們從源碼出發(fā),一探究竟。
1.下面是調(diào)度任務(wù)的入口方法。
// ScheduledThreadPoolExecutor#scheduleAtFixedRate 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(); // 將執(zhí)行任務(wù)和參數(shù)包裝成ScheduledFutureTask對(duì)象 ScheduledFutureTask<Void> sft = new ScheduledFutureTask<Void>(command, null, triggerTime(initialDelay, unit), unit.toNanos(period)); RunnableScheduledFuture<Void> t = decorateTask(command, sft); sft.outerTask = t; // 延遲執(zhí)行 delayedExecute(t); return t; }
這個(gè)方法主要做了兩個(gè)事情:
- 將執(zhí)行任務(wù)和參數(shù)包裝成ScheduledFutureTask對(duì)象
- 調(diào)用
delayedExecute
方法延遲執(zhí)行任務(wù)
2.延遲或周期性任務(wù)的主要執(zhí)行方法, 主要是將任務(wù)丟到隊(duì)列中,后續(xù)由工作線程獲取執(zhí)行。
// ScheduledThreadPoolExecutor#delayedExecute private void delayedExecute(RunnableScheduledFuture<?> task) { if (isShutdown()) reject(task); else { // 將任務(wù)丟到阻塞隊(duì)列中 super.getQueue().add(task); if (isShutdown() && !canRunInCurrentRunState(task.isPeriodic()) && remove(task)) task.cancel(false); else // 開啟工作線程,去執(zhí)行任務(wù),或者從隊(duì)列中獲取任務(wù)執(zhí)行 ensurePrestart(); } }
3.現(xiàn)在任務(wù)已經(jīng)在隊(duì)列中了,我們看下任務(wù)執(zhí)行的內(nèi)容是什么,還記得前面的包裝對(duì)象ScheduledFutureTask
類,它的實(shí)現(xiàn)類是ScheduledFutureTask
,繼承了Runnable類。
// ScheduledFutureTask#run方法 public void run() { // 是不是周期性任務(wù) boolean periodic = isPeriodic(); if (!canRunInCurrentRunState(periodic)) cancel(false); // 不是周期性任務(wù)的話, 直接調(diào)用一次下面的run else if (!periodic) ScheduledFutureTask.super.run(); // 如果是周期性任務(wù),則調(diào)用runAndReset方法,如果返回true,繼續(xù)執(zhí)行 else if (ScheduledFutureTask.super.runAndReset()) { // 設(shè)置下次調(diào)度時(shí)間 setNextRunTime(); // 重新執(zhí)行調(diào)度任務(wù) reExecutePeriodic(outerTask); } }
這里的關(guān)鍵就是看ScheduledFutureTask.super.runAndReset()
方法是否返回true,如果是true的話繼續(xù)調(diào)度。
4.runAndReset方法也很簡(jiǎn)單,關(guān)鍵就是看報(bào)異常如何處理。
// FutureTask#runAndReset protected boolean runAndReset() { if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return false; // 是否繼續(xù)下次調(diào)度,默認(rèn)false boolean ran = false; int s = state; try { Callable<V> c = callable; if (c != null && s == NEW) { try { // 執(zhí)行任務(wù) c.call(); // 執(zhí)行成功的話,設(shè)置為true ran = true; // 異常處理,關(guān)鍵點(diǎn) } catch (Throwable ex) { // 不會(huì)修改ran的值,最終是false,同時(shí)也不打印異常堆棧 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); } // 返回結(jié)果 return ran && s == NEW; }
- 關(guān)鍵點(diǎn)ran變量,最終返回是不是下次繼續(xù)調(diào)度執(zhí)行
- 如果拋出異常的話,可以看到不會(huì)修改ran為true。
總結(jié)
Java的ScheduledThreadPoolExecutor定時(shí)任務(wù)線程池所調(diào)度的任務(wù)中如果拋出了異常,并且異常沒有捕獲直接拋到框架中,會(huì)導(dǎo)致ScheduledThreadPoolExecutor定時(shí)任務(wù)不調(diào)度了。這個(gè)結(jié)論希望大家一定要記住,不然非???,關(guān)鍵是有時(shí)候測(cè)試環(huán)境、開發(fā)環(huán)境還無法復(fù)現(xiàn),有一定的隨機(jī)性,真的到了生產(chǎn)就完蛋了。
到此這篇關(guān)于詳解Java ScheduledThreadPoolExecutor的踩坑與解決方法的文章就介紹到這了,更多相關(guān)Java ScheduledThreadPoolExecutor內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java自帶定時(shí)任務(wù)ScheduledThreadPoolExecutor實(shí)現(xiàn)定時(shí)器和延時(shí)加載功能
- Java調(diào)度線程池ScheduledThreadPoolExecutor不執(zhí)行問題分析
- java高并發(fā)ScheduledThreadPoolExecutor類深度解析
- java高并發(fā)ScheduledThreadPoolExecutor與Timer區(qū)別
- java 定時(shí)器線程池(ScheduledThreadPoolExecutor)的實(shí)現(xiàn)
- Java使用quartz實(shí)現(xiàn)定時(shí)任務(wù)示例詳解
- Java實(shí)現(xiàn)定時(shí)任務(wù)最簡(jiǎn)單的3種方法
- Java項(xiàng)目實(shí)現(xiàn)定時(shí)任務(wù)的三種方法
- Java定時(shí)任務(wù)ScheduledThreadPoolExecutor示例詳解
相關(guān)文章
Spring引入外部屬性文件配置數(shù)據(jù)庫(kù)連接的步驟詳解
這篇文章主要介紹了Spring引入外部屬性文件配置數(shù)據(jù)庫(kù)連接的步驟詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01Java 并發(fā)編程ArrayBlockingQueue的實(shí)現(xiàn)
這篇文章主要介紹了Java 并發(fā)編程ArrayBlockingQueue的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02java 設(shè)計(jì)模式(DAO)的實(shí)例詳解
這篇文章主要介紹了java 設(shè)計(jì)模式(DAO)的實(shí)例詳解的相關(guān)資料,希望通過本文能幫助到大家,需要的朋友可以參考下2017-09-09springboot+springmvc+mybatis項(xiàng)目整合
這篇文章主要為大家詳細(xì)介紹了springboot+springmvc+mybatis項(xiàng)目的整合,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例
這篇文章主要介紹了java抓取鼠標(biāo)事件和鼠標(biāo)滾輪事件示例,需要的朋友可以參考下2014-05-05