Java線程池簡單總結

概述

線程可認爲是操做系統可調度的最小的程序執行序列,通常做爲進程的組成部分,同一進程中多個線程可共享該進程的資源(如內存等)。在單核處理器架構下,操做系統通常使用分時的方式實現多線程;在多核處理器架構下,多個線程可以作到真正的在不一樣處理核心並行處理。
不管使用何種方式實現多線程,正確使用多線程均可以提升程序性能,或是吞吐量,或是響應時間,甚至二者兼具。如何正確使用多線程涉及較多的理論及最佳實踐,本文沒法詳細展開,可參考如《Programming Concurrency on the JVM》等書籍。
本文主要內容爲簡單總結Java中線程池的相關信息。html

Java線程使用及特色

Java中提供Thread做爲線程實現,通常有兩種方式:java

  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接口的子接口ExecutorService
ExecutorService接口主要在兩方面擴展了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)

如今對各個參數做用進行總結:oracle

參數名稱 參數類型 參數用途
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線程及經常使用線程池的使用,對比常見線程池的特色。因爲本文側重於分析使用層面,並無深刻探究各線程池具體的代碼實現,此項可留後續繼續補充。

相關文章
相關標籤/搜索