1. 程式人生 > >Java執行緒池簡單總結

Java執行緒池簡單總結

概述

執行緒可認為是作業系統可排程的最小的程式執行序列,一般作為程序的組成部分,同一程序中多個執行緒可共享該程序的資源(如記憶體等)。在單核處理器架構下,作業系統一般使用分時的方式實現多執行緒;在多核處理器架構下,多個執行緒能夠做到真正的在不同處理核心並行處理。 無論使用何種方式實現多執行緒,正確使用多執行緒都可以提高程式效能,或是吞吐量,或是響應時間,甚至兩者兼具。如何正確使用多執行緒涉及較多的理論及最佳實踐,本文無法詳細展開,可參考如《Programming Concurrency on the JVM》等書籍。 本文主要內容為簡單總結Java中執行緒池的相關資訊。

Java執行緒使用及特點

Java中提供Thread作為執行緒實現,一般有兩種方式:

  1. 直接整合Thread類:
class PrimeThread extends Thread {
    long minPrime;
    PrimeThread(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
        . . .
    }
}
class Starter{
    public static void main(){
        PrimeThread p = new PrimeThread(143);
        p.start();
    }
}
  1. 實現Runnable 介面:
class PrimeRun implements Runnable {
    long minPrime;
    PrimeRun(long minPrime) {
        this.minPrime = minPrime;
    }

    public void run() {
        // compute primes larger than minPrime
        . . .
    }
}
class Starter{
    public static void main(){
        PrimeRun p = new PrimeRun(143);
        new Thread(p).start();
    }
}

執行緒是屬於作業系統的概念,Java中的多線執行緒實現一定會依託於作業系統支援。HotSpot虛擬機器中對多執行緒的實現實際上是使用了一對一的對映模型,即一個Java程序對映到一個輕量級程序(LWP)之中。在使用Threadstart方法後,HotSpot建立本地執行緒並與Java執行緒關聯。在此過程之中虛擬機器需要建立多個物件(如OSThread等)用於跟蹤執行緒狀態,後續需要進行執行緒初始化工作(如初始換ThreadLocalAllocBuffer物件等),最後啟動執行緒呼叫上文實現的run方法。 由此可見建立執行緒的成本較高,如果執行緒中run函式中業務程式碼執行時間非常短且消耗資源較少的情況下,可能出現建立執行緒成本大於執行真正業務程式碼的成本,這樣難以達到提升程式效能的目的。 由於建立執行緒成本較大,很容易想到通過複用已建立的執行緒已達到減少執行緒建立成本的方法,此時執行緒池就可以發揮作用。

Java執行緒池

Java執行緒池主要核心類(介面)為ExecutorExecutorServiceExecutors等,具體關係如下圖所示:

Executor介面

由以上類圖可見線上程池類結構體系中Executor作為最初始的介面,該介面僅僅規定了一個方法void execute(Runnable command),此介面作用為規定執行緒池需要實現的最基本方法為可執行實現了Runnable介面的任務,並且開發人員不需要關心具體的執行緒池實現(在實際使用過程中,仍需要根據不同任務特點選擇不同的執行緒池實現),將客戶端程式碼與執行客戶端程式碼的執行緒池解耦。

ExecutorService介面

Executor介面雖然完成了業務程式碼與執行緒池的解耦,但沒有提供任何與執行緒池互動的方法,並且僅僅支援沒有任何返回值的Runnable任務的提交,在實際業務實現中功能略顯不足。為了解決以上問題,JDK中增加了擴充套件Executor介面的子介面ExecutorServiceExecutorService介面主要在兩方面擴充套件了Executor介面:

  1. 提供針對執行緒池的多個管理方法,主要包括停止任務提交、停止執行緒池執行、判斷執行緒池是否停止執行及執行緒池中任務是否執行完成;
  2. 增加submit的多個過載方法,該方法可在提交執行任務時,返回給提交任務的執行緒一個Future物件,可通過該物件對提交的任務進行控制,如取消任務或獲取任務結果等(Future物件如何實現此功能另行討論)。

Executors工具類

Executors是主要為了簡化執行緒池的建立而提供的工具類,通過呼叫各靜態工具方法返回響應的執行緒池實現。通過對其方法的觀察可將其提供的工具方法歸為如下幾類:

  1. 建立ExecutorService物件的工具:又可細分為建立FixedThreadPoolSingleThreadPoolCachedThreadPoolWorkStealingPoolUnconfigurableExecutorServiceSingleThreadScheduledExecutorThreadScheduledExecutor
  2. 建立ThreadFactory物件;
  3. Runnable等物件封裝為Callable物件。

以上各工具方法中使用最廣泛的為newCachedThreadPoolnewFixedThreadPoolnewSingleThreadExecutor,這三個方法建立的ExecutorService物件均是其子類ThreadPoolExecutor(嚴格來說newSingleThreadExecutor方法返回的是FinalizableDelegatedExecutorService物件,其封裝了ThreadPoolExecutor,為何如此實現後文在做分析),下文著重分析ThreadPoolExecutor類。至於其他ExecutorService實現類,如ThreadScheduledExecutor本文不做詳細分析。

ThreadPoolExecutor

ThreadPoolExecutor類是執行緒池ExecutorService的重要實現類,在工具類Executors中構建的執行緒池物件,有大部分均是ThreadPoolExecutor實現。 ThreadPoolExecutor類提供多個構造引數對執行緒池進行配置,程式碼如下:

public ThreadPoolExecutor(int corePoolSize,
                        int maximumPoolSize,
                        long keepAliveTime,
                        TimeUnit unit,
                        BlockingQueue<Runnable> workQueue,
                        ThreadFactory threadFactory,
                        RejectedExecutionHandler handler)

現在對各個引數作用進行總結:

引數名稱 引數型別 引數用途
corePoolSize int 核心執行緒數,執行緒池中會一直保持該數量的執行緒,即使這些執行緒是空閒的狀態,如果設定allowCoreThreadTimeOut屬性(預設為false)為true,則空閒超過超時時間的核心執行緒可以被回收
maximumPoolSize int 最大執行緒數,當前執行緒池中可存在的最大執行緒數
keepAliveTime long 執行緒存活時間,噹噹前執行緒池中執行緒數大於核心執行緒數時,空閒執行緒等待新任務的時間,超過該時間則停止空閒執行緒
unit TimeUnit 時間單位,keepAliveTime屬性的時間單位
workQueue BlockingQueue<Runnable> 等待佇列,儲存待執行的任務
threadFactory ThreadFactory 執行緒工廠,執行緒池建立執行緒時s使用
handler RejectedExecutionHandler 拒絕執行處理器,當提交任務被拒絕(當等待佇列滿,且執行緒達到最大限制後)時呼叫

在使用該執行緒池時有一個重要的引數起效順序:

  1. 提交任務時,噹噹前執行的執行緒數小於核心執行緒時,則啟動新的執行緒執行任務;
  2. 提交任務時,當前執行執行緒數大於等於核心執行緒數,將當前任務加入等待佇列中;
  3. 將任務新增到等待佇列失敗時(如佇列滿),嘗試新建執行緒執行任務;
  4. 新建執行緒時,執行緒池關閉或達到最大執行緒數,則拒絕任務,呼叫handler進行處理。

ThreadFactory有預設的實現為Executors.DefaultThreadFactory,其建立執行緒主要額外工作為將新建的執行緒加入當前執行緒組,並且將執行緒的名稱置為pool-x-thread-y的形式。

ThreadPoolExecutor類通過內部類的形式提供了四種任務被拒絕時的處理器:AbortPolicyCallerRunsPolicyDiscardOldestPolicyDiscardPolicy

拒絕策略類 具體操作
AbortPolicy 丟擲RejectedExecutionException異常,拒絕執行任務
CallerRunsPolicy 在提交任務的執行緒執行當前任務,即在呼叫函式executesubmit的執行緒直接執行任務
DiscardOldestPolicy 直接取消當前等待佇列中最早的任務
DiscardPolicy 以靜默方式丟棄任務

ThreadPoolExecutor預設使用的是AbortPolicy處理策略,使用者可自行實現RejectedExecutionHandler介面自定義處理策略,本處不在贅述。

Executors對於ThreadPoolExecutor的建立

根據上文描述,Executors類提供了較多的關於建立或使用執行緒池的工具方法,此節重點總結其在建立ThreadPoolExecutor執行緒池的各方法。

newCachedThreadPool方法簇

newCachedThreadPool方法簇用於建立可快取任務的ThreadPoolExecutor執行緒池。包括兩個重構方法:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>(),
                                    threadFactory);
}

結合上文分析的ThreadPoolExecutor各構造引數,可總結如下:

  1. 核心執行緒數為0:沒有核心執行緒,即在沒有任務執行時所有執行緒均會被回收;
  2. 最大執行緒數為Integer.MAX_VALUE,即執行緒池中最大可存在的執行緒為Integer.MAX_VALUE,由於此值在通常情況下遠遠大於系統可新建的執行緒數,可簡單理解為此執行緒池不限制最大可建的執行緒數,此處可出現邏輯風險,在提交任務時可能由於超過系統處理能力造成無法再新建執行緒時會出現OOM異常,提示無法建立新的執行緒;
  3. 存活時間60秒:執行緒數量超過核心執行緒後,空閒60秒的執行緒將會被回收,根據第一條可知核心執行緒數為0,則本條表示所有執行緒空閒超過60秒均會被回收;
  4. 等待佇列SynchronousQueue:構建CachedThreadPool時,使用的等待佇列為SynchronousQueue型別,此型別的等待佇列較為特殊,可認為這是一個容量為0的阻塞佇列,在呼叫其offer方法時,如當前有消費者正在等待獲取元素,則返回true,否則返回false。使用此等待佇列可做到快速提交任務到空閒執行緒,沒有空閒執行緒時觸發新建執行緒;
  5. ThreadFactory引數:預設為DefaultThreadFactory,也可通過建構函式設定。

newFixedThreadPool方法簇

newFixedThreadPool方法簇用於建立固定執行緒數的ThreadPoolExecutor執行緒池。包括兩個構造方法:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory);
}

各構造引數總結:

  1. 核心執行緒數與最大執行緒數nThreads:構建的ThreadPoolExecutor核心執行緒數與最大執行緒數相等且均為nThreads,這說明當前執行緒池不會存在非核心執行緒,即不會存線上程的回收(allowCoreThreadTimeOut預設為false),隨著任務的提交,執行緒數增加到nThreads個後就不會變化;
  2. 存活時間為0:執行緒存在非核心執行緒,該時間沒有特殊效果;
  3. 等待佇列LinkedBlockingQueue:該等待佇列為LinkedBlockingQueue型別,沒有長度限制;
  4. ThreadFactory引數:預設為DefaultThreadFactory,也可通過建構函式設定。

newSingleThreadExecutor方法簇

newSingleThreadExecutor方法簇用於建立只包含一個執行緒的執行緒池。包括兩個構造方法:

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

結合上文分析的ThreadPoolExecutor各構造引數,可總結如下:

  1. 核心執行緒數與最大執行緒數1:當前執行緒池中有且僅有一個核心執行緒;
  2. 存活時間為0:當前執行緒池不存在非核心執行緒,不會存線上程的超時回收;
  3. 等待佇列LinkedBlockingQueue:該等待佇列為LinkedBlockingQueue型別,沒有長度限制;
  4. ThreadFactory引數:預設為DefaultThreadFactory,也可通過建構函式設定。

特殊說明,函式實際返回的物件型別並不是ThreadPoolExecutor而是FinalizableDelegatedExecutorService型別,為何如此設計在後文統一討論。

三種常見執行緒池的對比

上文總結了Executors工具類建立常見執行緒池的方法,現對三種執行緒池區別進行比較。

執行緒池型別 CachedThreadPool FixedThreadPool SingleThreadExecutor
核心執行緒數 0 nThreads(使用者設定) 1
最大執行緒數 Integer.MAX_VALUE nThreads(使用者設定) 1
非核心執行緒存活時間 60s 無非核心執行緒 無非核心執行緒
等待佇列最大長度 1 無限制 無限制
特點 提交任務優先複用空閒執行緒,沒有空閒執行緒則建立新執行緒 固定執行緒數,等待執行的任務均放入等待佇列 有且僅有一個執行緒在執行,等待執行任務放入等待佇列,可保證任務執行順序與提交順序一直
記憶體溢位 大量提交任務後,可能出現無法建立執行緒的OOM 大量提交任務後,可能出現記憶體不足的OOM 大量提交任務後,可能出現記憶體不足的OOM

三種類型的執行緒池與GC關係

原理說明

一般情況下JVM中的GC根據可達性分析確認一個物件是否可被回收(eligible for GC),而在執行的執行緒被視為‘GCRoot’。因此被在執行的執行緒引用的物件是不會被GC回收的。在ThreadPoolExecutor類中具有f非靜態內部類Worker,用於表示x當前執行緒池中的執行緒,並且根據Java語言規範An instance i of a direct inner class C of a class or interface O is associated with an instance of O, known as the immediately enclosing instance of i. The immediately enclosing instance of an object, if any, is determined when the object is created (§15.9.2).可知非靜態內部類物件具有外部包裝類物件的引用(此處也可通過檢視位元組碼來驗證),因此Worker類的物件即作為執行緒物件(‘GCRoot’)有持有外部類ThreadPoolExecutor物件的引用,則在其執行結束之前,外部內不會被Gc回收。 根據以上分析,再次觀察以上三個執行緒池:

  1. CachedThreadPool:沒有核心執行緒,且執行緒具有超時時間,可見在其引用消失後,等待任務執行結束且所有執行緒空閒回收後,GC開始回收此執行緒池物件;
  2. FixedThreadPool:核心執行緒數及最大執行緒數均為nThreads,並且在預設allowCoreThreadTimeOutfalse的情況下,其引用消失後,核心執行緒即使空閒也不會被回收,故GC不會回收該執行緒池;
  3. SingleThreadExecutor:預設與FixedThreadPool情況一致,但由於其語義為單執行緒執行緒池,JDK開發人員為其提供了FinalizableDelegatedExecutorService包裝類,在建立FixedThreadPool物件時實際返回的是FinalizableDelegatedExecutorService物件,該物件持有FixedThreadPool物件的引用,但FixedThreadPool物件並不引用FinalizableDelegatedExecutorService物件,這使得在FinalizableDelegatedExecutorService物件的外部引用消失後,GC將會對其進行回收,觸發finalize函式,而該函式僅僅簡單的呼叫shutdown函式關閉執行緒,是的所有當前的任務執行完成後,回收執行緒池中執行緒,則GC可回收執行緒池物件。

因此可得出結論,CachedThreadPoolSingleThreadExecutor的物件在不顯式呼叫shutdown函式(或shutdownNow函式),且其物件引用消失的情況下,可以被GC回收FixedThreadPool物件在不顯式呼叫shutdown函式(或shutdownNow函式),且其物件引用消失的情況下不會被GC回收,會出現記憶體洩露

實驗驗證

以上結論可使用實驗驗證:

public static void main(String[] args) throws InterruptedException {
        ExecutorService executorService = Executors.newCachedThreadPool();
        //ExecutorService executorService = Executors.newFixedThreadPool(1);
        //ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
        //執行緒引用置空
        executorService = null;
        Runtime.getRuntime().addShutdownHook(new Thread(() -> System.out.println("Shutdown.")));
        //等待執行緒超時,主要對CachedThreadPool有效
        Thread.sleep(100000);
        //手動觸發GC
        System.gc();
}

使用以上程式碼,分別建立三種不同的執行緒池,可發現最終FixedThreadPool不會打印出‘Shutdown.’,JVM沒有退出。另外兩種執行緒池均能退出JVM。因此無論使用什麼執行緒池執行緒池使用完畢後均呼叫shutdown以保證其最終會被GC回收是一個較為安全的程式設計習慣。

猜想及踩坑程式碼示例

根據以上的原理及程式碼分析,很容易提出如下問題:既然SingleThreadExecutor的實現方式可以自動完成執行緒池的關閉,為何不使用同樣的方式實現FixedThreadPool呢? 目前作者沒有找到確切的原因,此處引用兩個對此有所討論的兩個網址:王智超-理解SingleThreadExecutor及[Why doesn't all Executors factory methods wrap in a FinalizableDelegatedExecutorService?](https://stackoverflow.com/que...。 作者當前提出一種不保證正確的可能性:JDK開發人員可能重語義方面考慮將FixedThreadPool定義為可重新配置的執行緒池,SingleThreadExecutor定義為不可重新配置的執行緒池。因此沒有使用FinalizableDelegatedExecutorService物件包裝FixedThreadPool物件,將其控制權放到了程式設計師手中。 最後再分享一個關於SingleThreadExecutor的踩坑程式碼,改程式碼在程式設計過程中一般不會出現,但其中涉及較多知識點,不失為一個好的學習示例:

import java.util.concurrent.Callable;
import java.util.concurrent.Executors;

class Prog {
  public static void main(String[] args) {
    Callable<Long> callable = new Callable<Long>() {
      public Long call() throws Exception {
        // Allocate, to create some memory pressure.
        byte[][] bytes = new byte[1024][];
        for (int i = 0; i < 1024; i++) {
          bytes[i] = new byte[1024];
        }
        return 42L;
      }
    };
    for (;;) {
      Executors.newSingleThreadExecutor().submit(callable);
    }
  }
}

以上程式碼在設定-Xmx128m的虛擬機器進行執行,大概率會丟擲RejectedExecutionException異常,其原理與上文分析的GC回收有關,詳細分析可參考[Learning from bad code](https://www.farside.org.uk/20...

Executors對於ThreadPoolExecutor的建立的最佳實踐

以上總結了使用Executors建立常見執行緒池的方法,在簡單的使用中的確方便使用且減少的手動建立執行緒池的程式碼量,但在真正開發高併發程式時,其預設建立的執行緒由於遮蔽了底層引數,程式設計師難以真正理解其中可能出現的細節問題,包括記憶體溢位及拒絕策略等,故在使用中t推薦使用ThreadPoolExecutor等方式直接建立。此處可以參考《阿里巴巴Java開發手冊終極版v1.3.0》(六)併發處理的第4點。

總結

本文簡單總結了Java執行緒及常用執行緒池的使用,對比常見執行緒池的特點。由於本文側重於分析使用層面,並沒有深入探究各執行緒池具體的程式碼實現,此項可留後續繼續補充。