本身的事情本身作,線程異常處理

以前使用線程執行任務的時候,老是忽略了線程異常的處理,直到最近看書java

線程出現異常測試

  • 任務類:Task.javamysql

    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,將拋出一個異常sql

  • 線程測試類: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)
    複製代碼

    異常沒有被捕獲,只是在控制檯打印了異常,而且不影響後續任務的執行 emmmm這是爲何呢,捕獲不到異常就不知道程序出錯了,到時候哪天有個任務不正常排查都排查不到,這樣是要不得的。看一下Thread這個類,有個叫dispatchUncaughtException的方法,做用如其名,分發未捕獲的異常,把這段代碼揪出來:Thread#dispatchUncaughtExceptionbash

    private void dispatchUncaughtException(Throwable e) {
        getUncaughtExceptionHandler().uncaughtException(this, e);
    }
    複製代碼

    find usage是找不到該方法在哪裏調用的,由於這個方法只被JVM調用 Thread#getUncaughtExceptionHandler: 獲取UncaughtExceptionHandler接口實現類併發

    public UncaughtExceptionHandler getUncaughtExceptionHandler() {
        return uncaughtExceptionHandler != null ?
            uncaughtExceptionHandler : group;
    }
    複製代碼

UncaughtExceptionHandler是Thread中定義的接口,在Thread類中uncaughtExceptionHandler默認是null,所以該方法將返回group,即實現了UncaughtExceptionHandler接口的ThreadGroup類 UncaughtExceptionHandler#uncaughtException: ThreadGroup類的uncaughtException方法實現框架

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】進行賦值,所以將進入最後一層條件,將異常打印到控制檯中,對異常不作任何處理。 整個異常處理器調用鏈以下: socket

首先判斷默認異常處理器【defaultUncaughtExceptionHandler】是否是爲null,在判斷線程組異常處理器【group】是否是爲null,在判斷自定義異常處理器【uncaughtExceptionHandler】是否是爲null,都爲null則在控制檯打印異常

線程異常處理

分析了一下源碼就知道若是想對任務執行過程當中的異常進行處理一個就是讓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
    複製代碼

線程組異常捕獲處理器很適合爲線程進行分組處理的場景,每一個分組出現異常的處理方式不相同 設置完異常處理器後異常都能被捕獲了,可是不知道爲何設置異常處理器後任務的執行順序亂了,難道是由於爲每一個線程設置異常處理器的時間不一樣【想不通】

線程池異常處理

通常應用中線程都是經過線程池建立複用的,所以對線程池的異常處理就是爲線程池工廠類【ThreadFactory實現類】生成的線程添加異常處理器

  • 默認異常處理器

    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的設計理念"線程是獨立執行的代碼片段,線程的問題應該由線程本身來解決,而不要委託到外部"。所以在Java中,線程方法的異常【即任務拋出的異常】,應該在線程代碼邊界以內處理掉,而不該該在線程方法外面由其餘線程處理

線程執行Callable任務

前面介紹的是線程執行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方法,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 {
            // 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;
      //設置任務的狀態爲EXCEPTIONAL
      UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
      finishCompletion();
    }
}
複製代碼

看到catch這段代碼,當執行任務捕獲到異常的時候,會將任務的處理結果設置爲null,而且調用setException方法對捕獲的異常進行處理,由於setUncaughtExceptionHandler只對未捕獲的異常進行處理,FutureTask已經對異常進行了捕獲處理,所以調用setUncaughtExceptionHandler捕獲異常無效 對任務的執行結果調用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);
    }
}
複製代碼

執行結果將會將捕獲到的異常打印出來,執行結果:

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)
      //未完成等待任務執行完成
      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的狀態最終8被設置爲EXCEPTIONAL,所以方法將捕獲的任務拋出【new ExecutionException((Throwable)x)】
總結:
Callable任務拋出的異常能在代碼中經過try-catch捕獲到,可是只有調用get方法後才能捕獲到

image.png

附往期文章:歡迎你的閱讀、點贊、評論

併發相關
1.爲何阿里巴巴要禁用Executors建立線程池?

設計模式相關:
1. 單例模式,你真的寫對了嗎?
2. (策略模式+工廠模式+map)套餐 Kill 項目中的switch case

JAVA8相關:
1. 使用Stream API優化代碼
2. 親,建議你使用LocalDateTime而不是Date哦

數據庫相關:
1. mysql數據庫時間類型datetime、bigint、timestamp的查詢效率比較
2. 很高興!終於踩到了慢查詢的坑

高效相關:
1. 擼一個Java腳手架,一統團隊項目結構風格

日誌相關:
1. 日誌框架,選擇Logback Or Log4j2?
2. Logback配置文件這麼寫,TPS提升10倍

工程相關:
1. 閒來無事,動手寫一個LRU本地緩存
2. Redis實現點贊功能模塊
3. JMX可視化監控線程池
4. 權限管理 【SpringSecurity篇】
5. Spring自定義註解從入門到精通
6. java模擬登錄優酷
7. QPS這麼高,那就來寫個多級緩存吧
8. java使用phantomjs進行截圖

其餘:
1. 使用try-with-resources優雅關閉資源
2. 老闆,用float存儲金額爲何要扣我工資

相關文章
相關標籤/搜索