Java線程池異常處理機制


 

一、前言

      線程池技術是服務器端開發中常用的技術。不論是直接還是間接,各種服務器端功能的執行總是離不開線程池的調度。關於線程池的各種文章,多數是關注任務的創建和執行方面,對於異常處理和任務取消(包括線程池關閉)關注的偏少。接下來,本文將從 Java 原生線程、兩種主要線程池 ThreadPoolExecutor 和 ScheduledThreadPoolExecutor 這三方面介紹 Java 中線程的異常處理機制。

二、Thread

      在談線程池的異常處理之前,我們先來看 Java 中線程中的異常是如何被處理的。大家都知道如何創建一個線程任務,代碼1

  1.  
    Thread t = new Thread(() -> System.out.println("Execute in a thread"));
  2.  
    t.start();

    為了簡化代碼,這里使用了 Java 8 的 Lambda 表達式。() -> System.out.println("Execute in a thread") 等同於在 Runnable 中執行 System.out.println 方法。后面不再解釋。如果這個任務拋出了異常,那又會怎樣,代碼2

  1.  
    Thread t = new Thread(() -> System.out.println(1 / 0));
  2.  
    t.start();

      如果我們執行上面這段代碼,會在控制台上看到異常輸出。可能多數同學會對此不會覺得問題,但是問題在於,通常情況下絕大多數線上應用不會將控制台作為日志輸出地址,而是另有日志輸出。這種情況下,上面的代碼所拋出異常便會丟失。那為了將異常輸出到日志中,我們會這樣寫代碼:代碼3

  1.  
    Thread t = new Thread(() -> {
  2.  
    try {
  3.  
    System.out.println(1 / 0);
  4.  
    } catch (Exception e) {
  5.  
    LOGGER.error(e.getMessage(), e);
  6.  
    }
  7.  
    });
  8.  
    t.start();
  9.  
     

      這樣我們就能異常棧輸出到日志中,而不是控制台,從而避免異常的丟失。過了一段時間,問題又來了,可能好多線程任務默認的異常處理機制都是相同的。比如都是將異常輸出到日志文件。按照上面的寫法會造成重復代碼。雖然重復的不多,但是有代碼潔癖的小伙伴可能也會覺得不舒服。那我們該如何解決這個問題呢?其實 JDK 已經為我們想到了,Thread 類中有個接口 UncaughtExceptionHandler。通過實現這個接口,並調用 Thread.setUncaughtExceptionHandler(UncaughtExceptionHandler) 方法,我們就能為一個線程設置默認的異常處理機制,避免重復的 try...catch 了。除此以外,我們還可以通過 Thread.setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler) 設置全局的默認異常處理機制。此外,ThreadGroup 也實現了 UncaughtExceptionHandler 接口,所以通過 ThreadGroup 還可以為一組線程設置默認的異常處理機制。其實,之所以代碼2在執行之后我們能在控制台上看到異常,也是因為 UncaughtExceptionHandler 機制。ThreadGroup默認提供了異常處理機制如下:代碼4

  1.  
    public void uncaughtException(Thread t, Throwable e) {
  2.  
    if (parent != null) {
  3.  
    parent.uncaughtException(t, e);
  4.  
    } else {
  5.  
    Thread.UncaughtExceptionHandler ueh =
  6.  
    Thread.getDefaultUncaughtExceptionHandler();
  7.  
    if (ueh != null) {
  8.  
    ueh.uncaughtException(t, e);
  9.  
    } else if (!(e instanceof ThreadDeath)) {
  10.  
    // 最終執行如下代碼
  11.  
    System.err.print("Exception in thread \"" + t.getName() + "\" ");
  12.  
    e.printStackTrace(System.err);
  13.  
    }
  14.  
    }
  15.  
    }
  16.  
     

三、ThreadPoolExecutor

      在 Java 5 發布之后,線程池便開始越來越廣泛地用於創建並發任務。多數時候,當說到 Java 的線程池時,我們一般指的就是 ThreadPoolExecutor。那在 ThreadPoolExecutor 中是如何處理異常的呢?代碼5

  1.  
    Executors.newSingleThreadExecutor().execute(() -> {
  2.  
    throw new RuntimeException("My runtime exception");
  3.  
    });

     上面的代碼的異常處理機制其實同直接使用 Thread 是一樣的。所以也有同樣的問題,異常信息無法反映在日志文件中。解決這個問題的方法同上一節一樣:在每個 Runnable 中編寫 try ... catch 語句;或者使用 UncaughtExceptionHandler 機制。我們先來看如何為線程池中的工作線程設置 UncaughtExceptionHandler

  • 為線程池工作線程設置 UncaughtExceptionHandler

     簡單來說,就是通過 ThreadFactory。通過 ThreadPoolExecutor 的構造函數和 Executors 中的工具方法,我們都可以為新創建的線程池設置 ThreadFactoryThreadFactory 是個接口,它只定義了一個方法 Thread newThread(Runnable r)。在這個方法中,我們可以為新創建出來的線程設置 UncaughtExceptionHandler。當然,這樣寫起來顯得很麻煩,好在 Apache Commons 和 Google Guava 這兩個最有名的 Java 工具類庫都為我們提供了相應的類庫以簡化配置 ThreadFactory 的工作。下面以 Apache Commons 提供的 BasicThreadFactoryBuilder 為例,代碼6

  1.  
    ThreadFactory executorThreadFactory = new BasicThreadFactory.Builder()
  2.  
    .namingPattern( "task-scanner-executor-%d")
  3.  
    .uncaughtExceptionHandler( new LogUncaughtExceptionHandler(LOGGER))
  4.  
    .build();
  5.  
    Executors.newSingleThreadExecutor(executorThreadFactory);
  • UncaughtExceptionHandler 一定起作用嗎?

      此話怎講呢?其實 ThreadPoolExecutor 為執行並發任務提供了兩種方法:execute(Runnable) 和 submit(Callable/Runnable)。之前的代碼示例只演示了執行 execute(Runnable) 時的情況。那在設置了默認的 UncaughtExceptionHandler 之后,當執行 submit(Callable/Runnable) 方法,拋出拋異常之后有會如何?看下面的代碼代碼7

  1.  
    ThreadFactory threadFactory = new ThreadFactoryBuilder()
  2.  
    .setUncaughtExceptionHandler( new LogExceptionHandler())
  3.  
    .build();
  4.  
    Executors.newSingleThreadExecutor(threadFactory)
  5.  
    .submit(() -> {
  6.  
    throw new RuntimeException("test");
  7.  
    });

    上面的程序執行完之后,不會在控制台或日志中看到任何輸出,雖然設置了 UncaughtExceptionHandler。要弄清原因,就要看一下 ThreadPoolExecutor 的源代碼,代碼8

  1.  
    public Future<?> submit(Runnable task) {
  2.  
    if (task == null) throw new NullPointerException();
  3.  
    RunnableFuture< Void> ftask = newTaskFor(task, null);
  4.  
    execute(ftask);
  5.  
    return ftask;
  6.  
    }

   submit 方法是調用 execute 實現任務執行的。但是在調用 execute 之前,任務會被封裝進 FutureTask 類中,然后最終工作線程執行的是 FutureTask 中的 run 方法。代碼9:FutureTask.run

  1.  
    try {
  2.  
    result = c. call();
  3.  
    ran = true;
  4.  
    } catch (Throwable ex) {
  5.  
    result = null;
  6.  
    ran = false;
  7.  
    setException(ex);
  8.  
    }
  9.  
     
  10.  
    protected void setException(Throwable t) {
  11.  
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
  12.  
    outcome = t;
  13.  
    UNSAFE.putOrderedInt( this, stateOffset, EXCEPTIONAL); // final state
  14.  
    finishCompletion();
  15.  
    }
  16.  
    }

      由上面的代碼可以看出,不同於直接調用 execute 方法,調用 submit 方法后,如果任務拋出異常,會被 setException 方法賦給代表執行結果的 outcome 變量,而不會繼續拋出。因此,UncaughtExceptionHandler 也沒有機會處理。如果想知道 submit 的執行結果是成功還是失敗,必須調用 Future.get() 方法。

  • UncaughtExceptionHandler 是否適合在線程池中使用

      從上面的分析中可以看出,使用 UncaughtExceptionHandler,可以處理到使用 execute 方法執行任務所拋出的異常,但是對 submit 方法無效。那如果只是用 execute 方法,我們是否可以通過設置 UncaughtExceptionHandler 從而添加一種默認的異常處理機制,以避免重復的 try...catch 代碼呢?答案是不能。原因在於,如果在執行 execute 方法時不在 Runnable.run 方法中寫 try...catch 方法,自然異常會交由 UncaughtExceptionHandler 處理,但是,在這之前,線程的工作線程會因為異常而退出。雖然線程池會創建一個新的工作線程,但是如果這個步驟反復執行,效率自然會下降很多。

四、ScheduledThreadPoolExecutor

    ScheduledThreadPoolExecutor 是另一種常用的線程池,常用了執行延遲任務或定時任務。常用的方法為 scheduleXXX系列。那在這個線程池中異常是如何處理的呢?其實,如果看過前面的部分,到這里也基本能猜出來了。ScheduledThreadPoolExecutor 用來封裝任務的是 ScheduledFutureTaskScheduledFutureTask 是 FutureTask 的子類,所以,異常也會被復制給 outcome。但是,這里還是有一些差異的。在使用 ThreadPoolExecutor.submit 和 ScheduledThreadPoolExecutor.schedule 方法時,我們可以通過這兩個方法返回的 Future 來獲得執行結果,這包括正常結果,也包括異常結果。但是,對於 ScheduledThreadPoolExecutor.scheduleWithFixedDelay 和 scheduleAtFixedRate 這兩個方法,其返回的 Future 只會用來取消任務,而不是得到結果。原因也很容易理解,因為這兩個方法執行的是定時任務,是反復執行的。這也是為什么這兩個方法的任務定義使用了 Runnable 接口,而不是有返回值的 Callable 接口。因此,對於這兩個方法來說,在 Runnable.run 方法中加 try...catch 是必須的,否則很有可能出錯了卻毫不知情。

五、結論

  在Thread中,我們可以通過 UncaughtExceptionHandler 來實現默認的異常處理機制。但是在使用 ThreadPoolExecutor和 ScheduledThreadPoolExecutor 這兩個 JDK 最主要的線程池時,使用 UncaughtExceptionHandler 是不合適的。所以,try...catch 往往是不可避免的,否則你的任務很有可能失敗的悄無聲息。

 

原文鏈接:https://my.oschina.net/lifany/blog/884002

https://blog.csdn.net/u011635492/article/details/80328815


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM