線程池的異常處理

一、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);
 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章