線程可認爲是操做系統可調度的最小的程序執行序列,通常做爲進程的組成部分,同一進程中多個線程可共享該進程的資源(如內存等)。在單核處理器架構下,操做系統通常使用分時的方式實現多線程;在多核處理器架構下,多個線程可以作到真正的在不一樣處理核心並行處理。
不管使用何種方式實現多線程,正確使用多線程均可以提升程序性能,或是吞吐量,或是響應時間,甚至二者兼具。如何正確使用多線程涉及較多的理論及最佳實踐,本文沒法詳細展開,可參考如《Programming Concurrency on the JVM》等書籍。
本文主要內容爲簡單總結Java中線程池的相關信息。html
Java中提供Thread
做爲線程實現,通常有兩種方式:java
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(); } }
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)之中。在使用Thread
的start
方法後,HotSpot建立本地線程並與Java線程關聯。在此過程之中虛擬機須要建立多個對象(如OSThread
等)用於跟蹤線程狀態,後續須要進行線程初始化工做(如初始換ThreadLocalAllocBuffer
對象等),最後啓動線程調用上文實現的run
方法。
因而可知建立線程的成本較高,若是線程中run
函數中業務代碼執行時間很是短且消耗資源較少的狀況下,可能出現建立線程成本大於執行真正業務代碼的成本,這樣難以達到提高程序性能的目的。
因爲建立線程成本較大,很容易想到經過複用已建立的線程已達到減小線程建立成本的方法,此時線程池就能夠發揮做用。程序員
Java線程池主要核心類(接口)爲Executor
,ExecutorService
,Executors
等,具體關係以下圖所示:
編程
Executor
接口由以上類圖可見在線程池類結構體系中Executor
做爲最初始的接口,該接口僅僅規定了一個方法void execute(Runnable command)
,此接口做用爲規定線程池須要實現的最基本方法爲可運行實現了Runnable
接口的任務,而且開發人員不須要關心具體的線程池實現(在實際使用過程當中,仍須要根據不一樣任務特色選擇不一樣的線程池實現),將客戶端代碼與運行客戶端代碼的線程池解耦。緩存
ExecutorService
接口Executor
接口雖然完成了業務代碼與線程池的解耦,但沒有提供任何與線程池交互的方法,而且僅僅支持沒有任何返回值的Runnable
任務的提交,在實際業務實現中功能略顯不足。爲了解決以上問題,JDK中增長了擴展Executor
接口的子接口ExecutorService
。 ExecutorService
接口主要在兩方面擴展了Executor
接口:安全
submit
的多個重載方法,該方法可在提交運行任務時,返回給提交任務的線程一個Future
對象,可經過該對象對提交的任務進行控制,如取消任務或獲取任務結果等(Future對象如何實現此功能另行討論
)。Executors
工具類Executors
是主要爲了簡化線程池的建立而提供的工具類,經過調用各靜態工具方法返回響應的線程池實現。經過對其方法的觀察可將其提供的工具方法歸爲以下幾類:多線程
ExecutorService
對象的工具:又可細分爲建立FixedThreadPool
、SingleThreadPool
、CachedThreadPool
、WorkStealingPool
、UnconfigurableExecutorService
、SingleThreadScheduledExecutor
及ThreadScheduledExecutor
;ThreadFactory
對象;Runnable
等對象封裝爲Callable
對象。以上各工具方法中使用最普遍的爲newCachedThreadPool
、newFixedThreadPool
及newSingleThreadExecutor
,這三個方法建立的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 | 拒絕執行處理器,當提交任務被拒絕(當等待隊列滿,且線程達到最大限制後)時調用 |
在使用該線程池時有一個重要的參數起效順序:
handler
進行處理。ThreadFactory
有默認的實現爲Executors.DefaultThreadFactory
,其建立線程主要額外工做爲將新建的線程加入當前線程組,而且將線程的名稱置爲pool-x-thread-y
的形式。
ThreadPoolExecutor
類經過內部類的形式提供了四種任務被拒絕時的處理器:AbortPolicy
、CallerRunsPolicy
、DiscardOldestPolicy
及DiscardPolicy
。
拒絕策略類 | 具體操做 |
---|---|
AbortPolicy |
拋出RejectedExecutionException 異常,拒絕執行任務 |
CallerRunsPolicy |
在提交任務的線程執行當前任務,即在調用函數execute 或submit 的線程直接運行任務 |
DiscardOldestPolicy |
直接取消當前等待隊列中最先的任務 |
DiscardPolicy |
以靜默方式丟棄任務 |
ThreadPoolExecutor
默認使用的是AbortPolicy
處理策略,用戶可自行實現RejectedExecutionHandler
接口自定義處理策略,本處不在贅述。
根據上文描述,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
各構造參數,可總結以下:
Integer.MAX_VALUE
,即線程池中最大可存在的線程爲Integer.MAX_VALUE
,因爲此值在一般狀況下遠遠大於系統可新建的線程數,可簡單理解爲此線程池不限制最大可建的線程數,此處可出現邏輯風險,在提交任務時可能因爲超過系統處理能力形成沒法再新建線程時會出現OOM異常,提示沒法建立新的線程;SynchronousQueue
:構建CachedThreadPool
時,使用的等待隊列爲SynchronousQueue
類型,此類型的等待隊列較爲特殊,可認爲這是一個容量爲0的阻塞隊列,在調用其offer
方法時,如當前有消費者正在等待獲取元素,則返回true
,不然返回false
。使用此等待隊列可作到快速提交任務到空閒線程,沒有空閒線程時觸發新建線程;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); }
各構造參數總結:
nThreads
:構建的ThreadPoolExecutor
核心線程數與最大線程數相等且均爲nThreads
,這說明當前線程池不會存在非核心線程,即不會存在線程的回收(allowCoreThreadTimeOut
默認爲false
),隨着任務的提交,線程數增長到nThreads
個後就不會變化;LinkedBlockingQueue
:該等待隊列爲LinkedBlockingQueue
類型,沒有長度限制;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
各構造參數,可總結以下:
LinkedBlockingQueue
:該等待隊列爲LinkedBlockingQueue
類型,沒有長度限制;ThreadFactory
參數:默認爲DefaultThreadFactory,也可經過構造函數設置。特殊說明,函數實際返回的對象類型並非ThreadPoolExecutor
而是FinalizableDelegatedExecutorService
類型,爲什麼如此設計在後文統一討論。
上文總結了Executors
工具類建立常見線程池的方法,現對三種線程池區別進行比較。
線程池類型 | CachedThreadPool | FixedThreadPool | SingleThreadExecutor |
---|---|---|---|
核心線程數 | 0 | nThreads (用戶設定) |
1 |
最大線程數 | Integer.MAX_VALUE | nThreads (用戶設定) |
1 |
非核心線程存活時間 | 60s | 無非核心線程 | 無非核心線程 |
等待隊列最大長度 | 1 | 無限制 | 無限制 |
特色 | 提交任務優先複用空閒線程,沒有空閒線程則建立新線程 | 固定線程數,等待運行的任務均放入等待隊列 | 有且僅有一個線程在運行,等待運行任務放入等待隊列,可保證任務運行順序與提交順序一直 |
內存溢出 | 大量提交任務後,可能出現沒法建立線程的OOM | 大量提交任務後,可能出現內存不足的OOM | 大量提交任務後,可能出現內存不足的OOM |
通常狀況下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回收。
根據以上分析,再次觀察以上三個線程池:
nThreads
,而且在默認allowCoreThreadTimeOut
爲false
的狀況下,其引用消失後,核心線程即便空閒也不會被回收,故GC不會回收該線程池;FixedThreadPool
狀況一致,但因爲其語義爲單線程線程池,JDK開發人員爲其提供了FinalizableDelegatedExecutorService
包裝類,在建立FixedThreadPool
對象時實際返回的是FinalizableDelegatedExecutorService
對象,該對象持有FixedThreadPool
對象的引用,但FixedThreadPool
對象並不引用FinalizableDelegatedExecutorService
對象,這使得在FinalizableDelegatedExecutorService
對象的外部引用消失後,GC將會對其進行回收,觸發finalize
函數,而該函數僅僅簡單的調用shutdown
函數關閉線程,是的全部當前的任務執行完成後,回收線程池中線程,則GC可回收線程池對象。所以可得出結論,CachedThreadPool
及SingleThreadExecutor
的對象在不顯式調用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
建立常見線程池的方法,在簡單的使用中的確方便使用且減小的手動建立線程池的代碼量,但在真正開發高併發程序時,其默認建立的線程因爲屏蔽了底層參數,程序員難以真正理解其中可能出現的細節問題,包括內存溢出及拒絕策略等,故在使用中t推薦使用ThreadPoolExecutor
等方式直接建立。此處能夠參考《阿里巴巴Java開發手冊終極版v1.3.0》(六)併發處理的第4點。
本文簡單總結了Java線程及經常使用線程池的使用,對比常見線程池的特色。因爲本文側重於分析使用層面,並無深刻探究各線程池具體的代碼實現,此項可留後續繼續補充。