一、Java線程異常機制
在java多線程程序中,所有線程都不允許拋出未捕獲的checked exception,也就是說各個線程需要自己將checked exception處理掉,run方法上面進行了約束,不可以拋出異常(throws Exception)
public class Task implements Runnable {
@Override
public void run() { //run()方法上不可以拋出異常
System.out.println("任務開始執行");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int i = 10 /0; //這裏拋出RuntimeException()
System.out.println("任務執行結束");
}
}
public class ThreadException {
public static void main(String[] args) {
Thread t1 = new Thread(new Task());
//t1.setUncaughtExceptionHandler(new RuntimeExceptionHandle());
t1.start();
System.out.println("主線程執行結束!!!");
}
}
運行結果:
主線程執行結束!!!
任務開始執行
Exception in thread “Thread-0” java.lang.ArithmeticException: / by zero
at com.ljj.threadException.Task.run(Task.java:30)
at java.lang.Thread.run(Thread.java:748)
t1線程運行拋出異常不會影響主線程的執行,當此類異常產生時,子線程就會終結。我們無法在主線程中捕獲子線程拋出的異常進行處理,只能在run方法內部對業務邏輯進行try/catch。線程是獨立執行的代碼片斷,線程的問題應該由線程自己來解決,而不要委託到外部。
如果不在run()內部捕獲異常,該怎麼處理呢?
可以使用Thread.UncaughtExceptionHandler爲每個線程設置異常處理器,Thread.UncaughtExceptionHandler.uncaughtException()方法會在線程因未捕獲的異常而面臨死亡時被調用,上述子線程本身因爲異常終止打印到控制檯也是由於UncaughtExceptionHandler
實現UncaughtExceptionHandler接口並重寫uncaughtException方法,在uncaughtException方法中打印日誌即可
public class RuntimeExceptionHandle implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
//打印異常信息到日誌
System.out.println("異常處理器調用, 打印日誌: " + e);
}
}
放開上述代碼註釋,執行結果:
主線程執行結束!!!
任務開始執行
異常處理器調用, 打印日誌: java.lang.ArithmeticException: / by zero
子線程在拋出運行時異常,調用自定義的異常處理器,進行異常處理(日誌打印)
原理分析:
從JDK1.5開始,當一個線程因未捕獲的異常而即將終止時,JAVA虛擬機將使用Thread.getUncaughtExceptionHandler()查詢該線程以獲得其UncaughtExceptionHandler,並調用該handler的uncaughtException()方法,將線程和異常作爲參數傳遞。如果沒有,則搜索該線程的ThreadGroup的異常處理器。
ThreadGroup中的默認異常處理器實現是將處理工作逐層委託給上層的ThreadGroup,直到某個ThreadGroup的異常處理器能夠處理該異常,否則一直傳遞到頂層的ThreadGroup。頂層ThreadGroup的異常處理器委託給默認的系統處理器(如果默認的處理器存在,默認情況下爲空),否則把棧信息輸出到System.err
二、線程池異常
線程池中的異常如果處理不好的話,經常會出現日誌不打印,導致不能定位問題
public class ThreadPoolExecption {
private static ExecutorService executor = Executors.newFixedThreadPool(3);
public static void main(String[] args) {
Task task = new Task();
try {
executor.execute(task);
//executor.submit(task);
} catch (Exception e) {
System.out.println("捕獲線程池的線程異常");
}
}
}
運行結果:
任務開始執行
Exception in thread “pool-1-thread-1” java.lang.ArithmeticException: / by zero
at com.ljj.threadException.Task.run(Task.java:30)
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)
雖然在控制檯打印了異常堆棧信息,但是我們外層的catch並沒有捕獲這個運行時異常,在生產環境中,異常是不可能直接打印在控制檯的,而是打印到對應的日誌文件中去。
注意線程池執行有兩個方法,如果使用submit方法的話,堆棧信息在控制檯也不會打印。
分析線程池源碼:
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
/**
* addWorker方法部分內容
*/
w = new Worker(firstTask); //封裝成Worker對象
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
/**
* worker對象裏面的run方法部分內容
*/
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown); //執行改方法
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
首先ThreadPoolExecutor中execute方法會將傳入的task封裝成Worker對象,在進入Worker對象的run方法,發現異常被線程池捕獲了,但是最後在finally會執行 afterExecute(task, thrown)方法,該方法的方法體是空,裏面沒有任何邏輯。
三、線程池異常處理方法
1. run方法裏面try/catch所有處理邏輯
public void run() {
try {
//處理邏輯
} catch(Exeception e) {
//打印日誌
}
}
這是一種簡單而且不易出錯的線程池異常處理方式,推薦使用
2. 自定義異常處理器
由於線程池傳入的參數是Runnable不是Thread,執行一個個對應的任務,所以這裏我們需要使用ThreadFactory創建線程池
public class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(new RuntimeExceptionHandle());
return t;
}
}
繼承ThreadFactory,並重寫newThread(Runnable r)方法設置異常處理器,在異常處理器中捕獲並處理異常(打印日誌)
public class ThreadPoolExecption1 {
private static ExecutorService executor = Executors.newSingleThreadExecutor(new MyThreadFactory());
public static void main(String[] args) {
Task task = new Task();
executor.execute(task);
//executor.submit(task);
}
}
執行結果:
任務開始執行
異常處理器調用, 打印日誌: java.lang.ArithmeticException: / by zero
這種方法比較麻煩,更簡單的是在Thread類中設置一個靜態域,並將這個處理器設置爲默認異常處理器
public class ThreadPoolExeception2 {
private static ExecutorService executor = Executors.newFixedThreadPool(3);
public static void main(String[] args) {
Thread.setDefaultUncaughtExceptionHandler(new RuntimeExceptionHandle());
Task task = new Task();
executor.execute(task);
//executor.submit(task);
}
}
注意上述方法只有在執行execute方法纔可以捕獲異常進行處理,submit方法是不起作用的,沒有任何異常信息輸出。
3. 重寫ThreadPoolExecutor.afterExecute方法
前面分析過,線程池的線程在執行結束前肯定調用afterExecute方法,所有隻需要重寫該方法即可。
public class MyThreadPool extends ThreadPoolExecutor {
public MyThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
public void afterExecute(Runnable r, Throwable t) {
if(t != null) {
System.out.println("打印異常日誌:" + t);
}
}
}
繼承ThreadPoolExecutor 類並重寫afterExecute()方法
public class ThreadPoolExeception4 {
private static ThreadPoolExecutor executor = new MyThreadPool(1, 1, 0,
TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());
public static void main(String[] args) {
Task task = new Task();
executor.execute(task);
//executor.submit(task);
executor.shutdown();
}
}
執行結果:
任務開始執行
Exception in thread “pool-1-thread-1” 打印異常日誌:java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
at com.ljj.threadException.Task.run(Task.java:30)
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)
同樣,這種方式submit方法是不適用的
4. 使用submit執行任務
public class ThreadPoolExeception3 {
private static ExecutorService executor = Executors.newFixedThreadPool(3);
public static void main(String[] args) {
Task task = new Task();
try {
Future future = executor.submit(task);
System.out.println(future.get());
} catch (Exception e) {
System.out.println("捕獲線程池的線程異常:" + e);
}
}
}
執行結果:
任務開始執行
捕獲線程池的線程異常:java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
可以看到在使用submit執行任務,該方法將返回一個Future對象,不僅僅是任務的執行結果,異常也會被封裝到Future對象中,通過get()方法獲取。
源碼分析
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask); //封裝成FutureTask對象交給execute方法
return ftask;
}
由於調用ThreadPoolExecutor的execute方法,會被封裝成Worker對象,然後調用FutureTask對象的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
捕獲異常後調用setException(ex)方法,setExcetion首先是將一個異常信息賦值給一個全局變量outcome,並且將全局的任務狀態state字段通過CAS更新爲3(異常狀態)然後最後做一些清理工作
FutureTask.get()方法中,會對setException方法中設置的outcome和state做一些邏輯判斷,然後直接往上拋出了異常,所以我們就可以在主線程中捕獲這個異常
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);
}