您好,登錄后才能下訂單哦!
這篇文章主要介紹“怎么解決異步任務(wù)所導(dǎo)致的問題”,在日常操作中,相信很多人在怎么解決異步任務(wù)所導(dǎo)致的問題問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對(duì)大家解答”怎么解決異步任務(wù)所導(dǎo)致的問題”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!
前言
先簡單說下這段代碼,就是使用一個(gè)異步線程執(zhí)行一段業(yè)務(wù)邏輯,示例代碼如下:
// 前置邏輯 ..... Thread thread=new Thread(new Runnable() { @Override public void run() { try { // 異步線程執(zhí)行其他業(yè)務(wù)邏輯 } catch (Exception e) { // 不進(jìn)行任何代碼處理 } } }); thread.start();
憑著老程序員的經(jīng)驗(yàn),猜到可能是異步線程內(nèi)發(fā)生了異常,導(dǎo)致異步線程退出,不再繼續(xù)執(zhí)行。而又因?yàn)樯鲜龃a「吃掉」了異常,這就導(dǎo)致我們從外部看起來這個(gè)工程跑著跑著就不動(dòng)了,日志什么也沒了。
于是改造了一下,打印出相關(guān)異常日志,最終定位問題,原來是小姐姐造的數(shù)據(jù)存在問題,從而引發(fā) NPE 問題。
「不知道大家有沒有碰到過上面的情況,使用線程異步執(zhí)行相關(guān)邏輯,但是執(zhí)行到一半突然就像卡主一般,不再繼續(xù)往下執(zhí)行?!?/p>
總結(jié)起來分為下面三種情況:
異步任務(wù)長時(shí)間被阻塞
異步任務(wù)發(fā)生異常
異步任務(wù)異常被吃掉
異步任務(wù)長時(shí)間被阻塞
第一種,異步線程執(zhí)行任務(wù),這個(gè)任務(wù)需要通過網(wǎng)絡(luò)調(diào)用其他遠(yuǎn)端服務(wù)。假設(shè)服務(wù)端響應(yīng)的非常慢,而我們?cè)O(shè)置的網(wǎng)絡(luò)超時(shí)時(shí)間又很長,這就會(huì)導(dǎo)致這個(gè)線程長時(shí)間被阻塞。
假設(shè)異步任務(wù)偽碼如下:
ThreadPoolExecutor threadPool= ....; threadPool.execute(() -> { // 1.調(diào)用遠(yuǎn)端服務(wù) Socket socket....; // 2.設(shè)置超時(shí)時(shí)間 socket.setSoTimeout(60*1000); // 3.讀取服務(wù)端返回 socket.read(); });
上面程序中,如果服務(wù)端一直沒有返回,那么異步線程將會(huì)一直被阻塞,直到超時(shí)。
這種情況其實(shí)還好,我們無非等待一段時(shí)間,就可以看到異步線程繼續(xù)往下執(zhí)行任務(wù)。
舉一個(gè)極端的例子,假設(shè)上面的代碼沒有設(shè)置超時(shí)時(shí)間,而服務(wù)端一直沒有返回響應(yīng),「此時(shí)異步線程就會(huì)被一直阻塞」。
除了上面網(wǎng)絡(luò)讀取阻塞的例子,常見情況還有
執(zhí)行了長時(shí)間休眠,比如 TimeUnit.MINUTES.sleep(60)
內(nèi)部發(fā)生了死鎖
等等
如果異步線程長時(shí)間被阻塞,而異步任務(wù)執(zhí)行又比較頻繁,那么線程池內(nèi)可用線程將會(huì)被慢慢耗盡,此時(shí)后續(xù)任務(wù)就會(huì)被拒絕執(zhí)行。
解決辦法
其實(shí)非常簡單,首先我們使用 jstack 命令 「dump」 一下當(dāng)前 Java 應(yīng)用的線程堆棧情況,然后根據(jù)線程池名字定位相關(guān)線程即可。
網(wǎng)上隨便找了堆棧圖
如果沒有自定義線程池 ThreadFactory 參數(shù),那查找定位被阻塞線程就比較麻煩了。
所以創(chuàng)建線程池建議自定義 ThreadFactory 參數(shù),這對(duì)于后期排查問題非常有用。
異步任務(wù)異常未捕獲
上面的情況,異步線程其實(shí)還活著,只是被阻塞沒辦法執(zhí)行后續(xù)的邏輯。
那這一類情況呢,與上面不太一樣,由于異步任務(wù)內(nèi)部發(fā)生錯(cuò)誤,拋出異常,而代碼邏輯中又沒有進(jìn)行捕獲處理,從而導(dǎo)致線程提前異常退出。
異常退出偽碼如下:
// 1.創(chuàng)建執(zhí)行的任務(wù) Runnable runnable=new Runnable() { @Override public void run() { // 執(zhí)行前置邏輯 // 拋出異常 int i=100/0; // 執(zhí)行后置邏輯 } }; // 2.創(chuàng)建線程 Thread thread=new Thread(runnable); // 3.運(yùn)行異步線程 thread.start(); // 其他業(yè)務(wù)邏輯
上述代碼中,異步線程執(zhí)行到除零邏輯,將會(huì)拋出異常,然后異步線程將會(huì)異常退出。
「異步線程內(nèi)拋出的異常日志僅僅只會(huì)被打印到控制臺(tái),而不會(huì)被記錄到日志文件中。」
所以正常的業(yè)務(wù)日志中是見不到線程異常的日志,這就給了我們一種假象,異步線程看起來還在執(zhí)行任務(wù),其實(shí)它已經(jīng)掛了。
PS:上面的話可能不好理解,舉個(gè)例子,如果你使用 IDEA 執(zhí)行上面這段程序,異常日志將會(huì)被輸出到 IDEA 下方控制臺(tái)。
而如果我們?cè)?Linux 機(jī)器上執(zhí)行這段程序,異常日志僅僅只會(huì)顯示在當(dāng)前終端窗口上,一旦關(guān)閉當(dāng)前終端窗口,日志就沒。了。
如果想要保存這種日志,我們需要將 stdout 重定向到日志文件中,比如執(zhí)行以下命令:
-- 將 stdout 重定向輸出到文件中 nohup java xxxx > $STDOUT_FILE 2>&1 &
解決辦法
第一種解決辦法,其實(shí)很多讀者已經(jīng)想到了,異步線程內(nèi)使用 try..catch 語句捕獲所有異常即可。
「沒錯(cuò),就是這么簡單?!?/p>
不過這里提一點(diǎn),一般我們使用 try..catch僅僅只會(huì)捕獲 Exception異常。
那么極端情況下,異步線程內(nèi)如果拋出 Error,比如拋出了 java.lang.NoClassDefFoundError,此時(shí)是沒法捕獲,異步線程依舊會(huì)異常退出。
所以我們可以使用try..catch捕獲 Throwable,這樣及時(shí)發(fā)生 Error錯(cuò)誤,也會(huì)被捕獲。
不過個(gè)人覺得捕獲Exception異常就夠了,正常工程應(yīng)用很少會(huì)發(fā)生 Error錯(cuò)誤,所以我們只要了解有這個(gè)可能即可。
ps:之前同事上線一個(gè)應(yīng)用,使用異步線程執(zhí)行任務(wù),每次執(zhí)行到一半,都不再繼續(xù)執(zhí)行。
由于異步線程內(nèi)使用try..catch捕獲處理了 Exception異常,所以找了半天不知道什么問題。
最后,小黑哥排查 stdout 輸出日志,才發(fā)現(xiàn)異步線程發(fā)生 Error錯(cuò)誤。
這種解決本法需要我們主動(dòng)去捕獲異常,而下面第二種解決辦法,設(shè)置線程異常處理方法。
一旦設(shè)置完成,如果異步線程內(nèi)發(fā)生異常,線程退出之前將會(huì)調(diào)用異常處理方法。
我們拿 Thread 來講,其設(shè)置方法如下:
Runnable runnable=new Runnable() { @Override public void run() { int i=100/0; } }; Thread thread=new Thread(runnable); thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println(t.getName()+"發(fā)生異常"+e.getMessage()); } }); thread.start();
不過生產(chǎn)環(huán)境不建議直接使用 Thread,我們需要使用線程池代替。
線程池設(shè)置異常處理方法可以分為兩種,如果我們使用 ThreadPoolExecutor#execute執(zhí)行異步任務(wù),那我們需要在自定義線程池的時(shí)候,使用 ThreadFactory 設(shè)置。
ThreadPoolExecutor threadPool =new ThreadPoolExecutor( 5, 10, 60, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100), // 這里使用 Guava 的 ThreadFactoryBuilder 類,方便構(gòu)造 ThreadFactory new ThreadFactoryBuilder().setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { @Override public void uncaughtException(Thread t, Throwable e) { // 處理異常 } }).build() );
如果你當(dāng)前使用 ThreadPoolExecutor#submit執(zhí)行異步任務(wù),那就簡單了,我們可以直接通過 Future#get獲取到線程內(nèi)拋出的異常。
Future<?> future = threadPool.submit(new Callable<Object>() { @Override public Object call() throws Exception { return "小黑十一點(diǎn)半"; } }); try { future.get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { // 線程內(nèi)拋出異常將會(huì)被封裝在 ExecutionException 內(nèi) }
異步任務(wù)異常被吃掉
好了,終于到最后一種情況了,小黑哥這次碰到就是這種??。
這種情況具體來說就是異步線程內(nèi)使用 try..catch 語句捕獲了所有異常,但是沒有在 catch語句中進(jìn)行任何代碼處理。
Thread thread=new Thread(new Runnable() { @Override public void run() { try { int i=100/0; } catch (Exception e) { // 不進(jìn)行任何代碼處理 } } }); thread.start();
如上述代碼所示,catch語句中沒有進(jìn)行任何代碼處理。即使異步線程內(nèi)真發(fā)生了異常,也不會(huì)有任何提示,這個(gè)異常就像被吃掉一般。
到此,關(guān)于“怎么解決異步任務(wù)所導(dǎo)致的問題”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注億速云網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。