從使用到原理,探究Java線程池
什麼是線程池
當我們需要處理某個任務的時候,可以新建立一個線程,讓線程去執行任務。線程池的字面意思就是存放線程的池子,當我們需要處理某個任務的時候,可以從線程池裡取出一條線程去執行。
為什麼需要線程池
首先我們要知道不用線程池,直接建立線程有什麼弊端:
第一個是建立與銷毀線程的開銷,Java中的線程是映射到作業系統線程上的,頻繁地建立和銷毀線程會極大地損耗系統的性能。
線程會占用一定的記憶體空間,如果我們在同一時間内建立大量的線程執行任務,很有可能出現記憶體不足的情況。
為了解決這兩個問題我們引入線程池的概念,通過複用線程避免重複建立銷毀線程帶來的開銷,同時可以設定最大線程數,避免同時建立大量線程導緻記憶體溢出。
線程池的使用
1.線程池的核心參數
想掌握線程池首先要了解線程池構造函數的參數:
參數名 類型 含義
corePoolSize int 核心線程數
maxPoolSize int 最大線程數
keepAliveTime long 保持存活時間
workQueue BlockingQueue 任務存儲隊列
threadFactory ThreadFactory 當線程池需要新建立線程的時候,會通過ThreadFactory建立
Handler RejectedExecutionHandler 當線程池無法接受你送出的任務時所采取的拒絕政策
逐個解釋這些參數是很難了解的,這裡我結合一張線程池處理的流程圖進行講解:
當我們往線程池裡送出任務時,如果線程池内的線程數少于corePoolSize,則會直接建立新的線程處理任務;
如果線程池的線程數達到了corePoolSize,并且存儲隊列沒滿,則會把任務放到workQueue任務存儲隊列裡;
如果存儲隊列也滿了,但是線程數還沒有達到maxPoolSize,這個時候就會繼續建立線程執行任務。注意:這個時候線程池内的線程數已經超過了corePoolSize,超過corePoolSize的線程不會一直存活線上程池内,當他們閑下來時并超過keepAliveTime設定的時間後,就會被銷毀。
如果線程數已經達到了maxPoolSize,這個時候如果再來任務,線程池就采取Handler所指定的拒絕政策拒絕任務。
2.幾種常見的線程池分析
Java為我們提供了幾種常用的線程池,通過Executors類可以輕易地擷取它們。下面我們通過分析這幾種常用線程池的參數,了解這些線程池之間的異同。
newSingleThreadExecutor
從字面上也好了解,這是一個單線程的線程池,它的構造參數如下(建立的時候不需要傳參,這裡指的是下一層調用線程池構造函數時的傳參):
corePoolSize:1
maximumPoolSize(maxPoolSize):1
keepAliveTime:0L
workQueue:LinkedBlockingQueue
其他參數為預設值
大家按照照着上面的流程圖模拟送出任務走一遍,就知道為什麼這是一個單線程的線程池了。
當初次任務送出的時候,會建立一個線程執行任務;當送出第二個任務的時候,由于corePoolSize值為1,是以任務會放到任務隊列中。由于任務隊列選擇的是LinkedBlockingQueue,底層結構是連結清單,理論上可以存放幾乎無窮多的任務(預設的大小是Integer.MAX_VALUE),是以永遠不會觸發任務隊列已滿的條件,也就永遠不會繼續增加線程,是以該線程池能保持一個單線程的工作狀态。
如果這個唯一的線程因為異常結束了,線程池會建立一個新的線程補上。通過阻塞隊列,這個線程池能夠保證任務是按順序執行的。
newFixedThreadPool
這是一個固定線程數的線程池,它的構造參數如下:
corePoolSize:n
maximumPoolSize(maxPoolSize):n
如果了解了 SingleThreadExecutor 是如何限制隻有一條線程執行任務的話,那這裡固定線程數的原理也是一樣的,關鍵是限定 corePoolSize 和 maxPoolSize 的大小一樣,并使用幾乎無限容量LinkedBlockingQueue
newCachedThreadPool
可緩存的線程池,我了解的緩存是關于線程的緩存,它的構造參數如下:
corePoolSize:0
maximumPoolSize(maxPoolSize):Integer.MAX_VALUE
keepAliveTime:60L
workQueue:SynchronousQueue
由于corePoolSize為0,是以任務送出到該線程池後會直接到阻塞隊列。又由于阻塞隊列采用的是SynchronousQueue,這是一種不存儲任務的隊列,一旦獲得任務它就會分發給任務處理線程,是以直接觸發流程圖中第三個判斷框:如果目前線程數小于maxPoolSize就建立線程。由于maxPoolSize設定了一個很大的值,基本上可以無限地建立線程,具體的數量取絕于JVM所能建立的最大線程數。若線程空閑60秒沒任務處理便會被線程池回收。
該線程池在處理大量異步短連結任務的時候有較好的性能,在空閑的時候池内是沒有線程的,節省了系統的資源。
newScheduledThreadPool
corePoolSize:自定義
keepAliveTime:0
workQueue:DelayedWorkQueue
由于maxPoolSize設定為Integer.MAX_VALUE,該線程池可以無限建立線程,由于阻塞隊列選擇了DelayedWorkQueue,是以可以周期性地執行任務。
newWorkStealingPool
這個是JDK1.8新加入的線程池,底層使用的是ForkJoinPool。如果使用預設參數建立的話,該線程池能夠建立足夠多的線程以達到和系統相比對的并行處理能力。每個線程都有自己的工作隊列,如果目前線程工作完了,它會到别的工作隊列中“竊取”任務執行,充分地利用了CPU的多核能力。
阿裡巴巴關于建立線程池的約規
下面這段話搬運自阿裡巴巴Java開發手冊,相信大家看完上面的參數解釋以及各種線程池的異同後,就不難了解這段約規了:
(六)并發處理
- 【強制】線程池不允許使用Executors去建立,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明确線程池的運作規則,避免資源耗盡的風險。
說明:Executors傳回線程池的弊端如下:
1) FixedThreadPool和SingleThreadPool:
允許的請求隊列長度為Integer.MAX_VALUE,可能會堆積大量的請求,進而導緻OOM。
2) CacheThreadPool和ScheduledThreadPool:
允許建立的線程數量為Integer.MAX_VALUE,可能會建立大量的線程,導緻OOM。
-
線程池的數量設定為多少比較合适?
這個問題是沒有固定答案的,我們可以先通過業界權威給出的公式計算線程池的數量,然後通過壓測進一步确認具體的數量。
業界給出的指導公式是:
若任務是CPU密集型任務(比如說加密,計算哈希等),線程數可以設定為CPU核心數的1-2倍左右。
若任務是耗時IO型任務(比如說讀寫資料庫,檔案,網絡等),線程數的公式為:線程數 = CPU核心數 * (1 + 平均等待時間 / 平均處理時間)
這兩種不同設計都遵循着盡力壓榨CPU性能的原則。
-
線程池的五種狀态
線程池的五種狀态都寫在了ThreadPoolExecutor類中了,它們分别是:
RUNNING:接受新任務,并處理新任務
SHUTDOWN:不接受新任務,但是會處理隊列中的任務
STOP:不接受新任務,不處理隊列中的任務,中斷正在處理的任務
TIDYING:所有任務已經結束,workerCount為零,這時線程會轉到TIDYING狀态,并将運作terminated()鈎子方法
TERMINATED:terminated()運作完成
-
線程池運作的原理
我們先回顧一下如何新建立一個線程處理任務,看懂了再看線程池的原理就簡單了:
//首先把我們要放線上程裡運作的代碼在Runnable接口實作類的run方法中封裝好
class MyTask implements Runnable {
@Override
public void run() {
System.out.println("處理任務 + 1");
}
}
//然後建立一個線程,把該Runnable接口實作類作為構造參數傳給線程
public class Basic {
public static void main(String[] args) {
Thread thread = new Thread(new MyTask());
thread.start();
}
//最後調用線程的start方法運作,實際上調用的是Runnable的run方法
在上面的代碼中,實作了Runnable接口的執行個體傳入到線程類中,成為了線程對象的一個成員變量,線程運作的時候會調用該執行個體的run方法。
可以看到如果新建立一個線程來執行任務,任務會和線程耦合在一起。而線程池的關鍵原理在于它添加了一個阻塞隊列,把任務和線程解耦了
線上程池中,有一個worker的概念,這個概念解釋起來有點困難,你可以直接了解為worker就是一個線程勞工,它手上拿着任務,當調用線程池的runWorker()方法時,線程就會處理一個任務,詳細見下面代碼
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
while (task != null || (task = getTask()) != null) {//會到阻塞隊列中擷取任務
w.lock();
//...
try {
//執行任務
} finally {
//...
w.unlock();
}
}
//...
} finally {
//...
}
從代碼中可以看到線程池的關鍵代碼就是一個while循環,在while循環中會不斷地向阻塞隊列中擷取任務,擷取到了任務就執行。
參考:
慕課網《玩轉Java并發工具,精通JUC,成為并發多面手》課程
https://www.oschina.net/question/565065_86540 https://www.cnblogs.com/dolphin0520/p/3932921.html https://www.cnblogs.com/ok-wolf/p/7761755.html原文位址
https://www.cnblogs.com/tanshaoshenghao/p/12626462.html