天天看點

線程池執行過程中遇到異常會發生什麼,怎樣處理?

做個實驗:

public class ThreadExecutor {

 private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
   new ArrayBlockingQueue<>(200), new ThreadFactoryBuilder().setNameFormat("customThread %d").build());

 @Test
 public void test() {
  IntStream.rangeClosed(1, 5).forEach(i -> {
   try {
    Thread.sleep(100);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   threadPoolExecutor.execute(() -> {
     int j = 1/0;
  });});
 }
}           

建立一個隻有一個線程的線程池,每隔0.1s送出一個任務,任務中是一個1/0的計算。

Exception in thread "customThread 0" java.lang.ArithmeticException: / by zero
 at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at java.lang.Thread.run(Thread.java:748)
Exception in thread "customThread 1" java.lang.ArithmeticException: / by zero
 at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at java.lang.Thread.run(Thread.java:748)
Exception in thread "customThread 2" java.lang.ArithmeticException: / by zero
 at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at java.lang.Thread.run(Thread.java:748)
Exception in thread "customThread 3" java.lang.ArithmeticException: / by zero
 at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at java.lang.Thread.run(Thread.java:748)
Exception in thread "customThread 4" java.lang.ArithmeticException: / by zero
 at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at java.lang.Thread.run(Thread.java:748)
Exception in thread "customThread 5" java.lang.ArithmeticException: / by zero
 at thread.ThreadExecutor.lambda$null$0(ThreadExecutor.java:25)
 at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
 at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 at java.lang.Thread.run(Thread.java:748)           

可見每次執行的線程都不一樣,之前的線程都沒有複用。原因是因為出現了未捕獲的異常。

我們把異常捕獲試試:

public class ThreadExecutor {

 private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
   new ArrayBlockingQueue<>(200), new ThreadFactoryBuilder().setNameFormat("customThread %d").build());

 @Test
 public void test() {
  IntStream.rangeClosed(1, 5).forEach(i -> {
   try {
    Thread.sleep(100);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   threadPoolExecutor.execute(() -> {
    try {
     int j = 1 / 0;
    } catch (Exception e) {
     System.out.println(Thread.currentThread().getName() +" "+ e.getMessage());
    }
   });
  });
 }
}           
customThread 0 / by zero
customThread 0 / by zero
customThread 0 / by zero
customThread 0 / by zero
customThread 0 / by zero           

可見當異常捕獲了,線程就可以複用了。

問題來了,我們的代碼中異常不可能全部捕獲

如果要捕獲那些沒被業務代碼捕獲的異常,可以設定Thread類的uncaughtExceptionHandler屬性。這時使用ThreadFactoryBuilder會比較友善,ThreadFactoryBuilder是guava提供的ThreadFactory生成器。

new ThreadFactoryBuilder()
.setNameFormat("customThread %d")
.setUncaughtExceptionHandler((t, e) -> System.out.println(t.getName() + "發生異常" + e.getCause()))
.build()           

修改之後:

public class ThreadExecutor {

 private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
   new ArrayBlockingQueue<>(200),
   new ThreadFactoryBuilder()
     .setNameFormat("customThread %d")
     .setUncaughtExceptionHandler((t, e) -> System.out.println("UncaughtExceptionHandler捕獲到:" + t.getName() + "發生異常" + e.getMessage()))
     .build());

 @Test
 public void test() {
  IntStream.rangeClosed(1, 5).forEach(i -> {
   try {
    Thread.sleep(100);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }

   threadPoolExecutor.execute(() -> {
    System.out.println("線程" + Thread.currentThread().getName() + "執行");
    int j = 1 / 0;
   });
  });
 }
}           
線程customThread 0執行
UncaughtExceptionHandler捕獲到:customThread 0發生異常/ by zero
線程customThread 1執行
UncaughtExceptionHandler捕獲到:customThread 1發生異常/ by zero
線程customThread 2執行
UncaughtExceptionHandler捕獲到:customThread 2發生異常/ by zero
線程customThread 3執行
UncaughtExceptionHandler捕獲到:customThread 3發生異常/ by zero
線程customThread 4執行
UncaughtExceptionHandler捕獲到:customThread 4發生異常/ by zero           

可見,結果并不是我們想象的那樣,線程池中原有的線程沒有複用!是以通過UncaughtExceptionHandler想将異常吞掉使線程複用這招貌似行不通。它隻是做了一層異常的保底處理。

将excute改成submit試試可見,結果并不是我們想象的那樣,線程池中原有的線程沒有複用!是以通過UncaughtExceptionHandler想将異常吞掉使線程複用這招貌似行不通。它隻是做了一層異常的保底處理。

将excute改成submit試試

public class ThreadExecutor {

 private static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 60, TimeUnit.SECONDS,
   new ArrayBlockingQueue<>(200),
   new ThreadFactoryBuilder()
     .setNameFormat("customThread %d")
     .setUncaughtExceptionHandler((t, e) -> System.out.println("UncaughtExceptionHandler捕獲到:" + t.getName() + "發生異常" + e.getMessage()))
     .build());

 @Test
 public void test() {
  IntStream.rangeClosed(1, 5).forEach(i -> {
   try {
    Thread.sleep(100);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }

   Future<?> future = threadPoolExecutor.submit(() -> {
    System.out.println("線程" + Thread.currentThread().getName() + "執行");
    int j = 1 / 0;
   });
   try {
    future.get();
   } catch (InterruptedException e) {
    e.printStackTrace();
   } catch (ExecutionException e) {
    e.printStackTrace();
   }
  });
 }
}           
線程customThread 0執行
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
線程customThread 0執行
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
線程customThread 0執行
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
線程customThread 0執行
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero
線程customThread 0執行
java.util.concurrent.ExecutionException: java.lang.ArithmeticException: / by zero           

通過submit送出線程可以屏蔽線程中産生的異常,達到線程複用。當get()執行結果時異常才會抛出。

原因是通過submit送出的線程,當發生異常時,會将異常儲存,待future.get();時才會抛出。

這是Futuretask的部分run()方法,看setException:

public void run() {
    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);
        }
    } 
}

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        finishCompletion();
    }
}           

将異常存在outcome對象中,沒有抛出,再看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);
}           

當outcome是異常時才抛出。

總結

1、線程池中線程中異常盡量手動捕獲

2、通過設定ThreadFactory的UncaughtExceptionHandler可以對未捕獲的異常做保底處理,通過execute送出任務,線程依然會中斷,而通過submit送出任務,可以擷取線程執行結果,線程異常會在get執行結果時抛出。

如果你想開發小程式或者了解更多小程式的内容,可以通過專業開發公司,來幫助你實作開發需求:

廈門在乎科技

-專注

廈門小程式開發公司

、APP開發、網站開發、H5小遊戲開發