<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
啟動一個Java程式,本質上是執行某個Java類的main方法。我們寫一個死迴圈程式,跑起來,然後執行jvisualvm
進行觀察
可以看到這個Java程序中,一共有11個執行緒,其中10個守護執行緒,1個使用者執行緒。我們main方法中的程式碼,就跑在一個名為main
的執行緒中。當Java程序中跑著的所有執行緒都是守護執行緒時,JVM就會退出。
在單執行緒的場景下,如果程式碼執行到某個位置時丟擲了異常,會看到控制檯列印出異常的堆疊資訊。但在多執行緒的場景下,子執行緒中發生的異常,不一定就能及時的將異常資訊列印出來。
我曾經在工作中遇到過一次,採用CompletableFuture.runAsync
非同步處理耗時任務時,任務處理過程中出現異常,然而紀錄檔中沒有任何關於異常的資訊。時隔許久,重新溫習了執行緒中的例外處理機制,加深了對執行緒工作原理的理解,特此記錄。
我們知道,Java程式的執行,是先經由javac
將Java原始碼編譯成class位元組碼檔案,然後由JVM載入並解析class檔案,隨後從主類的main方法開始執行。當一個執行緒在執行過程中丟擲了未捕獲異常時,會由JVM呼叫這個執行緒物件上的dispatchUncaughtException
方法,進行例外處理。
// Thread類中 private void dispatchUncaughtException(Throwable e) { getUncaughtExceptionHandler().uncaughtException(this, e); }
原始碼很好理解,先獲取一個UncaughtExceptionHandler
例外處理器,然後通過呼叫這個例外處理器的uncaughtException
方法來對異常進行處理。(下文用縮寫ueh
來表示UncaughtExceptionHandler
)
ueh
是個 啥呢?其實就是定義在Thread
內部的一個介面,用作例外處理。
@FunctionalInterface public interface UncaughtExceptionHandler { /** * Method invoked when the given thread terminates due to the * given uncaught exception. * <p>Any exception thrown by this method will be ignored by the * Java Virtual Machine. * @param t the thread * @param e the exception */ void uncaughtException(Thread t, Throwable e); }
再來看下Thread
物件中的getUncaughtExceptionHandler
方法
public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; }
先檢視當前這個Thread
物件是否有設定自定義的ueh
物件,若有,則由其對異常進行處理,否則,由當前Thread
物件所屬的執行緒組(ThreadGroup
)進行例外處理。我們點開原始碼,容易發現ThreadGroup
類本身實現了Thread.UncaughtExceptionHandler
介面,也就是說ThreadGroup
本身就是個例外處理器。
public class ThreadGroup implements Thread.UncaughtExceptionHandler { private final ThreadGroup parent; .... }
假設我們在main
方法中丟擲一個異常,若沒有對main
執行緒設定自定義的ueh
物件,則交由main
執行緒所屬的ThreadGroup
來處理異常。我們看下ThreadGroup
是怎麼處理異常的:
public void uncaughtException(Thread t, Throwable e) { if (parent != null) { parent.uncaughtException(t, e); } else { Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) { ueh.uncaughtException(t, e); } else if (!(e instanceof ThreadDeath)) { System.err.print("Exception in thread "" + t.getName() + "" "); e.printStackTrace(System.err); } } }
這部分原始碼也比較簡短。首先是檢視當前ThreadGroup
是否擁有父級的ThreadGroup
,若有,則呼叫父級ThreadGroup
進行例外處理。否則,呼叫靜態方法Thread.getDefaultUncaughtExceptionHandler()
獲取一個預設的ueh
物件。
若預設的ueh
物件不為空,則由這個預設的ueh
物件進行例外處理;否則,當異常不是ThreadDeath
時,直接將當前執行緒的名字,和異常的堆疊資訊,通過標準錯誤輸出(System.err
)列印到控制檯。
我們隨便執行一個main
方法,看一下執行緒的情況
可以看到,main
執行緒屬於一個同樣名為main
的ThreadGroup
,而這個main
的ThreadGroup
,其父級ThreadGroup
名為system
,而這個system
的ThreadGroup
,沒有父級了,它就是根ThreadGroup
。
由此可知,main
執行緒中丟擲的未捕獲異常,最終會交由名為system
的ThreadGroup
進行例外處理,而由於沒有設定預設的ueh
物件,異常資訊會通過System.err
輸出到控制檯。
接下來,我們通過最樸素的方式(new
一個Thread
),在main
執行緒中建立一個子執行緒,在子執行緒中編寫能丟擲異常的程式碼,進行觀察
public static void main(String[] args) { Thread thread = new Thread(() -> { System.out.println(3 / 0); }); thread.start(); }
子執行緒中的異常資訊被列印到了控制檯。例外處理的流程就是我們上面描述的那樣。
所以,正常來說,如果沒有對某個執行緒設定特定的ueh
物件;也沒有呼叫靜態方法Thread.setDefaultUncaughtExceptionHandler
設定全域性預設的ueh
物件。那麼,在任意一個執行緒的執行過程中丟擲未捕獲異常時,異常資訊都會被輸出到控制檯(當異常是ThreadDeath
時則不會進行輸出,但通常來說,異常都不是ThreadDeath
,不過這個細節要注意下)。
如何設定自定義的ueh
物件來進行例外處理?根據上面的分析可知,有2種方式
Thread
物件,呼叫其setUncaughtExceptionHandler
方法,設定一個ueh
物件。注意這個ueh
物件只對這個執行緒起作用Thread.setDefaultUncaughtExceptionHandler()
設定一個全域性預設的ueh
物件。這樣設定的ueh
物件會對所有執行緒起作用當然,由於ThreadGroup
本身可以充當ueh
,所以其實還可以實現一個ThreadGroup
子類,重寫其uncaughtException
方法進行例外處理。
若一個執行緒沒有進行任何設定,當在這個執行緒內丟擲異常後,預設會將執行緒名稱和異常堆疊,通過System.err
進行輸出。
執行緒的例外處理機制,用一個流程圖表示如下:
在實際的開發中,我們經常會使用執行緒池來進行多執行緒的管理和控制,而不是通過new
來手動建立Thread
物件。
對於Java中的執行緒池ThreadPoolExecutor
,我們知道,通常來說有兩種方式,可以向執行緒池提交任務:
execute
submit
其中execute
方法沒有返回值,我們通過execute
提交的任務,只需要提交該任務給執行緒池執行,而不需要獲取任務的執行結果。而submit
方法,會返回一個Future
物件,我們通過submit
提交的任務,可以通過這個Future
物件,拿到任務的執行結果。
我們分別嘗試如下程式碼:
public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.execute(() -> { System.out.println(3 / 0); }); }
public static void main(String[] args) { ExecutorService threadPool = Executors.newSingleThreadExecutor(); threadPool.submit(() -> { System.out.println(3 / 0); }); }
容易得到如下結果:
通過execute
方法提交的任務,異常資訊被列印到控制檯;通過submit
方法提交的任務,沒有出現異常資訊。
我們稍微跟一下ThreadPoolExecutor
的原始碼,當使用execute
方法提交任務時,在runWorker
方法中,會執行到下圖紅框的部分
在上面的程式碼執行完畢後,由於異常被throw
了出來,所以會由JVM捕捉到,並呼叫當前子執行緒的dispatchUncaughtException
方法進行處理,根據上面的分析,最終異常堆疊會被列印到控制檯。
多扯幾句別的。
上面跟原始碼時,注意到Worker
是ThreadPoolExecutor
的一個內部類,也就是說,每個Worker
都會隱式的持有ThreadPoolExecutor
物件的參照(內部類的相關原理請自行補課)。每個Worker
在執行時(在不同的子執行緒中執行)都能夠對ThreadPoolExecutor
物件(通常來說這個物件是在main
執行緒中被維護)中的屬性進行存取和修改。Worker
實現了Runnable
介面,並且其run
方法實際是呼叫的ThreadPoolExecutor
上的runWorker
方法。在新建一個Worker
時,會建立一個新的Thread
物件,並把當前Worker
的參照傳遞給這個Thread
物件,隨後呼叫這個Thread
物件的start
方法,則開始在這個Thread
中(子執行緒中)執行這個Worker
。
Worker(Runnable firstTask) { setState(-1); // inhibit interrupts until runWorker this.firstTask = firstTask; this.thread = getThreadFactory().newThread(this); }
ThreadPoolExecutor
中的addWorker
方法
再次跟原始碼時,加深了對ThreadPoolExecutor
和Worker
體系的理解和認識。
它們之間有一種巢狀依賴的關係。每個Worker
裡持有一個Thread
物件,這個Thread
物件又是以這個Worker
物件作為Runnable
,而Worker
又是ThreadPoolExecutor
的內部類,這意味著每個Worker
物件都會隱式的持有其所屬的ThreadPoolExecutor
物件的參照。每個Worker
的run
方法, 都跑在子執行緒中,但是這些Worker
跑在子執行緒中時,能夠對ThreadPoolExecutor
物件的屬性進行存取和修改(每個Worker
的run
方法都是呼叫的runWorker
,所以runWorker
方法是跑在子執行緒中的,這個方法中會對執行緒池的狀態進行存取和修改,比如當前子執行緒執行過程中丟擲異常時,會從ThreadPoolExecutor
中移除當前Worker
,並啟一個新的Worker
)。而通常來說,ThreadPoolExecutor
物件的參照,我們通常是在主執行緒中進行維護的。
反正就是這中間其實有點騷東西,沒那麼簡單。需要多跟幾次原始碼,多自己打斷點進行debug,debug過程中可以通過IDEA的Evaluate Expression
功能實時觀察當前方法執行時所處的執行緒環境(Thread.currentThread
)。
扯得有點遠了,現在回到正題。上面說了呼叫ThreadPoolExecutor
中的execute
方法提交任務,子執行緒中出現異常時,異常會被丟擲,列印在控制檯,並且當前Worker
會被執行緒池回收,並重啟一個新的Worker
作為替代。那麼,呼叫submit
時,異常為何就沒有被列印到控制檯呢?
我們看一下原始碼:
public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; }
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) { return new FutureTask<T>(runnable, value); }
通過呼叫submit
提交的任務,被包裝了成了一個FutureTask
物件,隨後會將這個FutureTask
物件,通過execute
方法提交給執行緒池,並返回FutureTask
物件給主執行緒的呼叫者。
也就是說,submit
方法實際做了這幾件事
Runnable
,包裝成FutureTask
execute
方法提交這個FutureTask
(實際還是通過execute
提交的任務)FutureTask
作為返回值,返回給主執行緒的呼叫者關鍵就在於FutureTask
,我們來看一下
public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable }
// Executors中 public static <T> Callable<T> callable(Runnable task, T result) { if (task == null) throw new NullPointerException(); return new RunnableAdapter<T>(task, result); }
static final class RunnableAdapter<T> implements Callable<T> { final Runnable task; final T result; RunnableAdapter(Runnable task, T result) { this.task = task; this.result = result; } public T call() { task.run(); return result; } }
通過submit
方法傳入的Runnable
,通過一個介面卡RunnableAdapter
轉化為了Callable
物件,並最終包裝成為一個FutureTask
物件。這個FutureTask
,又實現了Runnable
和Future
介面
於是我們看下FutureTask
的run
方法(因為最終是將包裝後的FutureTask
提交給執行緒池執行,所以最終會執行FutureTask
的run
方法)
protected void setException(Throwable t) { if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { outcome = t; UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state finishCompletion(); } }
可以看到,異常資訊只是被簡單的設定到了FutureTask
的outcome
欄位上。並沒有往外拋,所以這裡其實相當於把異常給生吞了,catch
塊中捕捉到異常後,既沒有列印異常的堆疊,也沒有把異常繼續往外throw
。所以我們無法在控制檯看到異常資訊,在實際的專案中,此種場景下的異常資訊也不會被輸出到紀錄檔檔案。這一點要特別注意,會加大問題的排查難度。
那麼,為什麼要這樣處理呢?
因為我們通過submit
提交任務時,會拿到一個Future
物件
public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; }
我們可以在稍後,通過Future
物件,來獲知任務的執行情況,包括任務是否成功執行完畢,任務執行後返回的結果是什麼,執行過程中是否出現異常。
所以,通過submit
提交的任務,實際會把任務的各種狀態資訊,都封裝在FutureTask
物件中。當最後呼叫FutureTask
物件上的get
方法,嘗試獲取任務執行結果時,才能夠看到異常資訊被列印出來。
public V get() throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) s = awaitDone(false, 0L); return report(s); }
private V report(int s) throws ExecutionException { Object x = outcome; if (s == NORMAL) return (V)x; if (s >= CANCELLED) throw new CancellationException(); throw new ExecutionException((Throwable)x); // 異常會通過這一句被丟擲來 }
ThreadPoolExecutor
的execute
方法提交的任務,出現異常後,異常會在子執行緒中被丟擲,並被JVM捕獲,並呼叫子執行緒的dispatchUncaughtException
方法,進行例外處理,若子執行緒沒有任何特殊設定,則異常堆疊會被輸出到System.err
,即異常會被列印到控制檯上。並且會從執行緒池中移除當前Worker
,並另啟一個新的Worker
作為替代。ThreadPoolExecutor
的submit
方法提交的任務,任務會先被包裝成FutureTask
物件,出現異常後,異常會被生吞,並暫存到FutureTask
物件中,作為任務執行結果的一部分。異常資訊不會被列印,該子執行緒也不會被執行緒池移除(因為異常在子執行緒中被吞了,沒有丟擲來)。在呼叫FutureTask
上的get
方法時(此時一般是在主執行緒中了),異常才會被丟擲,觸發主執行緒的例外處理,並輸出到System.err
其他的執行緒池場景
比如:
ScheduledThreadPoolExecutor
實現延遲任務或者定時任務(週期任務),分析過程也是類似。這裡給個簡單結論,當呼叫scheduleAtFixedRate
方法執行一個週期任務時(任務會被包裝成FutureTask
(實際是ScheduledFutureTask
,是FutureTask
的子類)),若週期任務中出現異常,異常會被生吞,異常資訊不會被列印,執行緒不會被回收,但是週期任務執行這一次後就不會繼續執行了。ScheduledThreadPoolExecutor
繼承了ThreadPoolExecutor
,所以其也是複用了ThreadPoolExecutor
的那一套邏輯。CompletableFuture
的runAsync
提交任務,底層是通過ForkJoinPool
執行緒池進行執行,任務會被包裝成AsyncRun
,且會返回一個CompletableFuture
給主執行緒。當任務出現異常時,處理方式和ThreadPoolExecutor
的submit
類似,異常堆疊不會被列印。只有在CompletableFuture
上呼叫get
方法嘗試獲取結果時,異常才會被列印。到此這篇關於Java執行緒的例外處理機制詳情的文章就介紹到這了,更多相關Java執行緒例外處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45