之前使用線程執(zhí)行任務的時候,總是忽略了線程異常的處理,直到最近看書
線程出現(xiàn)異常測試類
-
任務類:Task.java
public class Task implements Runnable { private int i; public Task(int i) { this.i = i; } @Override public void run() { if (i == 5) { //System.out.println("throw exception"); throw new IllegalArgumentException(); } System.out.println(i); } }如果i==5,將拋出一個異常
-
線程測試類:TaskTest.java
public class TestTask { public static void main(String[] args) { int i = 0; while (true) { if (i == 10) break; try { new Thread(new Task(i++)).start(); } catch (Exception e) { System.out.println("catch exception..."); } } } }通過使用try-catch,嘗試對拋出的異常進行捕獲
-
測試結果
Connected to the target VM, address: '127.0.0.1:64551', transport: 'socket' 0 1 2 3 4 6 7 8 9 Exception in thread "pool-1-thread-1" java.lang.IllegalArgumentException at com.h2t.study.thread.Task.run(Task.java:21) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)異常沒有被捕獲,只是在控制臺打印了異常,并且不影響后續(xù)任務的執(zhí)行
emmmm這是為什么呢,捕獲不到異常就不知道程序出錯了,到時候哪天有個任務不正常排查都排查不到,這樣是要不得的??匆幌耇hread這個類,有個叫dispatchUncaughtException的方法,作用如其名,分發(fā)未捕獲的異常,把這段代碼揪出來:Thread#dispatchUncaughtExceptionprivate void dispatchUncaughtException(Throwable e) { getUncaughtExceptionHandler().uncaughtException(this, e); }find usage是找不到該方法在哪里調用的,因為這個方法只被JVM調用
Thread#getUncaughtExceptionHandler:獲取UncaughtExceptionHandler接口實現(xiàn)類public UncaughtExceptionHandler getUncaughtExceptionHandler() { return uncaughtExceptionHandler != null ? uncaughtExceptionHandler : group; }
UncaughtExceptionHandler是Thread中定義的接口,在Thread類中uncaughtExceptionHandler默認是null,因此該方法將返回group,即實現(xiàn)了UncaughtExceptionHandler接口的ThreadGroup類
UncaughtExceptionHandler#uncaughtException:ThreadGroup類的uncaughtException方法實現(xiàn)
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);
}
}
}
因為在Thread類中沒有對group【parent】和defaultUncaughtExceptionHandler【Thread.getDefaultUncaughtExceptionHandler】進行賦值,因此將進入最后一層條件,將異常打印到控制臺中,對異常不做任何處理。
整個異常處理器調用鏈如下:

首先判斷默認異常處理器【defaultUncaughtExceptionHandler】是不是為null,在判斷線程組異常處理器【group】是不是為null,在判斷自定義異常處理器【uncaughtExceptionHandler】是不是為null,都為null則在控制臺打印異常
線程異常處理
分析了一下源碼就知道如果想對任務執(zhí)行過程中的異常進行處理一個就是讓ThreadGroup不為null,另外一種思路就是讓UncaughtExceptionHandler類型的變量值不為null。
-
異常處理器:ExceptionHandler.java
private static class ExceptionHandler implements Thread.UncaughtExceptionHandler { @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("異常捕獲到了:" + e); } } -
設置默認異常處理器
Thread.setDefaultUncaughtExceptionHandler((t, e) -> System.out.println("異常捕獲到 了: " + e)); int i = 0; while (true) { if (i == 10) break; Thread thread = new Thread(new Task(i++)); thread.start(); }打印結果:
0 2 1 3 9 6 7 4 異常捕獲到了:java.lang.IllegalArgumentException 8通過設置默認異常就不需要為每個線程都設置一次了
-
設置自定義異常處理器
Thread t = new Thread(new Task(i++)); t.setUncaughtExceptionHandler(new ExceptionHandler());打印結果:
0 2 4 異常捕獲到了:java.lang.IllegalArgumentException 6 1 3 7 9 8 -
設置線程組異常處理器
MyThreadGroup myThreadGroup = new MyThreadGroup("測試線程線程組"); Thread t = new Thread(myThreadGroup, new Task(i++))自定義線程組:MyThreadGroup.java
private static class MyThreadGroup extends ThreadGroup { public MyThreadGroup(String name) { super(name); } @Override public void uncaughtException(Thread t, Throwable e) { System.out.println("捕獲到異常了:" + e); } }打印結果:
1 2 0 4 3 6 7 8 9 捕獲到異常了:java.lang.IllegalArgumentException
線程組異常捕獲處理器很適合為線程進行分組處理的場景,每個分組出現(xiàn)異常的處理方式不相同
設置完異常處理器后異常都能被捕獲了,但是不知道為什么設置異常處理器后任務的執(zhí)行順序亂了,難道是因為為每個線程設置異常處理器的時間不同【想不通】
線程池異常處理
一般應用中線程都是通過線程池創(chuàng)建復用的,因此對線程池的異常處理就是為線程池工廠類【ThreadFactory實現(xiàn)類】生成的線程添加異常處理器
-
默認異常處理器
Thread.setDefaultUncaughtExceptionHandler(new ExceptionHandler()); ExecutorService es = Executors.newCachedThreadPool(); es.execute(new Task(i++)) -
自定義異常處理器
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); threadPoolExecutor.setThreadFactory(new MyThreadFactory()); threadPoolExecutor.execute(new Task(i++));自定義工廠類:MyThreadFactory.java
private static class MyThreadFactory implements ThreadFactory { @Override public Thread newThread(Runnable r) { Thread t = new Thread(); //自定義UncaughtExceptionHandler t.setUncaughtExceptionHandler(new ExceptionHandler()); return t; } }
設計原則,為什么要由線程自身進行捕獲
來自JVM的設計理念"線程是獨立執(zhí)行的代碼片斷,線程的問題應該由線程自己來解決,而不要委托到外部"。因此在Java中,線程方法的異?!炯慈蝿諕伋龅漠惓!?,應該在線程代碼邊界之內處理掉,而不應該在線程方法外面由其他線程處理
線程執(zhí)行Callable任務
前面介紹的是線程執(zhí)行Runnable類型任務的情況,眾所周知,還有一種有返回值的Callable任務類型
測試代碼:TestTask.java
public class TestTask {
public static void main(String[] args) {
int i = 0;
while (true) {
if (i == 10) break;
FutureTask<Integer> task = new FutureTask<>(new CallableTask(i++));
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
private static class ExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("異常捕獲到了:" + e);
}
}
}
打印結果:
Disconnected from the target VM, address: '127.0.0.1:64936', transport: 'socket'
0
1
2
3
4
6
7
8
9
觀察結果,異常沒有被捕獲,thread.setUncaughtExceptionHandler(new ExceptionHandler())方法設置無效,emmmmm,這又是為什么呢,在問為什么就是十萬個為什么兒童了。查看FutureTask的run方法,F(xiàn)utureTask#run:
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} 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
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
FutureTask#setException
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
//將異常設置給outcome變量
outcome = t;
//設置任務的狀態(tài)為EXCEPTIONAL
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
看到catch這段代碼,當執(zhí)行任務捕獲到異常的時候,會將任務的處理結果設置為null,并且調用setException方法對捕獲的異常進行處理,因為setUncaughtExceptionHandler只對未捕獲的異常進行處理,F(xiàn)utureTask已經(jīng)對異常進行了捕獲處理,因此調用setUncaughtExceptionHandler捕獲異常無效
對任務的執(zhí)行結果調用get方法:
int i = 0;
while (true) {
if (i == 10) break;
FutureTask<Integer> task = new FutureTask<>(new CallableTask(i++));
Thread thread = new Thread(task);
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
//打印結果
try {
System.out.println(task.get());
} catch (Exception e) {
System.out.println("異常被抓住了, e: " + e);
}
}
執(zhí)行結果將會將捕獲到的異常打印出來,執(zhí)行結果:
0
1
2
3
4
異常被抓住了, e: java.util.concurrent.ExecutionException: java.lang.IllegalArgumentException
6
7
Disconnected from the target VM, address: '127.0.0.1:50900', transport: 'socket'
8
9
FutureTask#get
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
//未完成等待任務執(zhí)行完成
s = awaitDone(false, 0L);
return report(s);
}
FutureTask#report
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);
}
outcome在setException方法中被設置為了異常,并且s為state的狀態(tài)最終被設置為EXCEPTIONAL,因此方法將捕獲的任務拋出【new ExecutionException((Throwable)x)】
總結:
Callable任務拋出的異常能在代碼中通過try-catch捕獲到,但是只有調用get方法后才能捕獲到

附往期文章:歡迎你的閱讀、點贊、評論
并發(fā)相關
1.為什么阿里巴巴要禁用Executors創(chuàng)建線程池?
設計模式相關:
1. 單例模式,你真的寫對了嗎?
2. (策略模式+工廠模式+map)套餐 Kill 項目中的switch case
JAVA8相關:
1. 使用Stream API優(yōu)化代碼
2. 親,建議你使用LocalDateTime而不是Date哦
數(shù)據(jù)庫相關:
1. mysql數(shù)據(jù)庫時間類型datetime、bigint、timestamp的查詢效率比較
2. 很高興!終于踩到了慢查詢的坑
高效相關:
1. 擼一個Java腳手架,一統(tǒng)團隊項目結構風格
日志相關:
1. 日志框架,選擇Logback Or Log4j2?
2. Logback配置文件這么寫,TPS提高10倍
工程相關:
1. 閑來無事,動手寫一個LRU本地緩存
2. Redis實現(xiàn)點贊功能模塊
3. JMX可視化監(jiān)控線程池
4. 權限管理 【SpringSecurity篇】
5. Spring自定義注解從入門到精通
6. java模擬登陸優(yōu)酷
7. QPS這么高,那就來寫個多級緩存吧
8. java使用phantomjs進行截圖
其他:
1. 使用try-with-resources優(yōu)雅關閉資源
2. 老板,用float存儲金額為什么要扣我工資