Java多執行緒18:執行緒池

五月的倉頡發表於2015-10-06

使用執行緒池與不使用執行緒池的差別

先來看一下使用執行緒池與不使用執行緒池的差別,第一段程式碼是使用執行緒池的:

public static void main(String[] args)
{
    long startTime = System.currentTimeMillis();
    final List<Integer> l = new LinkedList<Integer>();
    ThreadPoolExecutor tp = new ThreadPoolExecutor(100, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(20000));
    final Random random = new Random();
    for (int i = 0; i < 20000; i++)
    {
        tp.execute(new Runnable()
        {
            public void run()
            {
                l.add(random.nextInt());
            }
        });
    }
    tp.shutdown();
    try
    {
        tp.awaitTermination(1, TimeUnit.DAYS);
    }
    catch (InterruptedException e)
    {
        e.printStackTrace();
    }
    System.out.println(System.currentTimeMillis() - startTime);
    System.out.println(l.size());
}

接著是不使用執行緒池的:

public static void main(String[] args)
{
    long startTime = System.currentTimeMillis();
    final List<Integer> l = new LinkedList<Integer>();
    final Random random = new Random();
    for (int i = 0; i < 20000; i++)
    {
        Thread thread = new Thread()
        {
            public void run()
            {
                l.add(random.nextInt());
            }
        };
        thread.start();
        try
        {
            thread.join();
        }
        catch (InterruptedException e)
        {
            e.printStackTrace();
        }
    }
    System.out.println(System.currentTimeMillis() - startTime);
    System.out.println(l.size());
}

執行一下,我這裡第一段程式碼使用了執行緒池的時間是194ms,第二段程式碼不使用執行緒池的時間是2043ms。這裡預設的執行緒池中的執行緒數是100,如果把這個數量減小,雖然系統的處理資料能力變弱了,但是速度卻更快了。當然這個例子很簡單,但也足夠說明問題了。

 

執行緒池的作用

執行緒池的作用就2個:

1、減少了建立和銷燬執行緒的次數,每個工作執行緒都可以被重複利用,可執行多個任務

2、可以根據系統的承受能力,調整執行緒池中工作執行緒的資料,防止因為消耗過多的記憶體導致伺服器崩潰

使用執行緒池,要根據系統的環境情況,手動或自動設定執行緒數目。少了系統執行效率不高,多了系統擁擠、佔用記憶體多。用執行緒池控制數量,其他執行緒排隊等候。一個任務執行完畢,再從佇列中取最前面的任務開始執行。若任務中沒有等待任務,執行緒池這一資源處於等待。當一個新任務需要執行,如果執行緒池中有等待的工作執行緒,就可以開始執行了,否則進入等待佇列。

 

執行緒池類結構

畫了一張圖表示執行緒池的類結構圖:

這張圖基本簡單代表了執行緒池類的結構:

1、最頂級的介面是Executor,不過Executor嚴格意義上來說並不是一個執行緒池而只是提供了一種任務如何執行的機制而已

2、ExecutorService才可以認為是真正的執行緒池介面,介面提供了管理執行緒池的方法

3、下面兩個分支,AbstractExecutorService分支就是普通的執行緒池分支,ScheduledExecutorService是用來建立定時任務的

 

ThreadPoolExecutor六個核心引數

這篇文章重點講的就是執行緒池ThreadPoolExecutor,開頭也演示過ThreadPoolExecutor的使用了。

下面來看一下ThreadPoolExecutor完整構造方法的簽名,簽名中包含了六個引數,是ThreadPoolExecutor的核心,對這些引數的理解、配置、調優對於使用好執行緒池是非常重要的。因此接下來需要逐一理解每個引數的具體作用。先看一下構造方法簽名:

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

1、corePoolSize

核心池的大小。在建立了執行緒池之後,預設情況下,執行緒池中沒有任何執行緒,而是等待有任務到來才建立執行緒去執行任務。預設情況下,在建立了執行緒池之後,執行緒池鐘的執行緒數為0,當有任務到來後就會建立一個執行緒去執行任務

2、maximumPoolSize

池中允許的最大執行緒數,這個參數列示了執行緒池中最多能建立的執行緒數量,當任務數量比corePoolSize大時,任務新增到workQueue,當workQueue滿了,將繼續建立執行緒以處理任務,maximumPoolSize表示的就是wordQueue滿了,執行緒池中最多可以建立的執行緒數量

3、keepAliveTime

只有當執行緒池中的執行緒數大於corePoolSize時,這個引數才會起作用。當執行緒數大於corePoolSize時,終止前多餘的空閒執行緒等待新任務的最長時間

4、unit

keepAliveTime時間單位

5、workQueue

儲存還沒來得及執行的任務

6、threadFactory

執行程式建立新執行緒時使用的工廠

7、handler

由於超出執行緒範圍和佇列容量而使執行被阻塞時所使用的處理程式

 

corePoolSize與maximumPoolSize舉例理解

上面的內容,其他應該都相對比較好理解,只有corePoolSize和maximumPoolSize需要多思考。這裡要特別再舉例以四條規則解釋一下這兩個引數:

1、池中執行緒數小於corePoolSize,新任務都不排隊而是直接新增新執行緒

2、池中執行緒數大於等於corePoolSize,workQueue未滿,首選將新任務加入workQueue而不是新增新執行緒

3、池中執行緒數大於等於corePoolSize,workQueue已滿,但是執行緒數小於maximumPoolSize,新增新的執行緒來處理被新增的任務

4、池中執行緒數大於大於corePoolSize,workQueue已滿,並且執行緒數大於等於maximumPoolSize,新任務被拒絕,使用handler處理被拒絕的任務

ThreadPoolExecutor的使用很簡單,前面的程式碼也寫過例子了。通過execute(Runnable command)方法來發起一個任務的執行,通過shutDown()方法來對已經提交的任務做一個有效的關閉。儘管執行緒池很好,但我們要注意JDK API的一段話:

強烈建議程式設計師使用較為方便的Executors工廠方法Executors.newCachedThreadPool()(無界執行緒池,可以進行執行緒自動回收)、Executors.newFixedThreadPool(int)(固定大小執行緒池)和Executors.newSingleThreadExecutor()(單個後臺執行緒),它們均為大多數使用場景預定義了設定。

所以,跳開對ThreadPoolExecutor的關注(還是那句話,有問題查詢JDK API),重點關注一下JDK推薦的Executors。

 

Executors

個人認為,執行緒池的重點不是ThreadPoolExecutor怎麼用或者是Executors怎麼用,而是在合適的場景下使用合適的執行緒池,所謂"合適的執行緒池"的意思就是,ThreadPoolExecutor的構造方法傳入不同的引數,構造出不同的執行緒池,以滿足使用的需要

下面來看一下Executors為使用者提供的幾種執行緒池:

1、newSingleThreadExecutos()   單執行緒執行緒池

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

單執行緒執行緒池,那麼執行緒池中執行的執行緒數肯定是1。workQueue選擇了無界的LinkedBlockingQueue,那麼不管來多少任務都排隊,前面一個任務執行完畢,再執行佇列中的執行緒。從這個角度講,第二個引數maximumPoolSize是沒有意義的,因為maximumPoolSize描述的是排隊的任務多過workQueue的容量,執行緒池中最多隻能容納maximumPoolSize個任務,現在workQueue是無界的,也就是說排隊的任務永遠不會多過workQueue的容量,那maximum其實設定多少都無所謂了

2、newFixedThreadPool(int nThreads)   固定大小執行緒池

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

固定大小的執行緒池和單執行緒的執行緒池異曲同工,無非是讓執行緒池中能執行的執行緒程式設計了手動指定的nThreads罷了。同樣,由於是選擇了LinkedBlockingQueue,因此其實第二個引數maximumPoolSize同樣也是無意義的

3、newCachedThreadPool()   無界執行緒池

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

無界執行緒池,意思是不管多少任務提交進來,都直接執行。無界執行緒池採用了SynchronousQueue,採用這個執行緒池就沒有workQueue容量一說了,只要新增進去的執行緒就會被拿去用。既然是無界執行緒池,那執行緒數肯定沒上限,所以以maximumPoolSize為主了,設定為一個近似的無限大Integer.MAX_VALUE。 另外注意一下,單執行緒執行緒池和固定大小執行緒池執行緒都不會進行自動回收的,也即是說保證提交進來的任務最終都會被處理,但至於什麼時候處理,就要看處理能力了。但是無界執行緒池是設定了回收時間的,由於corePoolSize為0,所以只要60秒沒有被用到的執行緒都會被直接移除

 

談談workQueue

上面三種執行緒池都提到了一個概念,workQueue,也就是排隊策略。排隊策略描述的是,當前執行緒大於corePoolSize時,執行緒以什麼樣的方式排隊等待被執行。

排隊有三種策略:直接提交、有界佇列、無界佇列。

談談後兩種,JDK使用了無界佇列LinkedBlockingQueue作為WorkQueue而不是有界佇列ArrayBlockingQueue,儘管後者可以對資源進行控制,但是個人認為,使用有界佇列相比無界佇列有三個缺點:

1、使用有界佇列,corePoolSize、maximumPoolSize兩個引數勢必要根據實際場景不斷調整以求達到一個最佳,這勢必給開發帶來極大的麻煩,必須經過大量的效能測試。所以乾脆就使用無界佇列,任務永遠新增到佇列中,不會溢位,自然maximumPoolSize也沒什麼用了,只需要根據系統處理能力調整corePoolSize就可以了

2、防止業務突刺。尤其是在Web應用中,某些時候突然大量請求的到來都是很正常的。這時候使用無界佇列,不管早晚,至少保證所有任務都能被處理到。但是使用有界佇列呢?那些超出maximumPoolSize的任務直接被丟掉了,處理地慢還可以忍受,但是任務直接就不處理了,這似乎有些糟糕

3、不僅僅是corePoolSize和maximumPoolSize需要相互調整,有界佇列的佇列大小和maximumPoolSize也需要相互折衷,這也是一塊比較難以控制和調整的方面

當然,最後還是那句話,就像Comparable和Comparator的對比、synchronized和ReentrantLock,再到這裡的無界佇列和有界佇列的對比,看似都有一個的優點稍微突出一些,但是這絕不是鼓勵大家使用一個而不使用另一個,任何東西都需要根據實際情況來,當然在一開始的時候可以重點考慮那些看上去優點明顯一點的

 

四種拒絕策略

所謂拒絕策略之前也提到過了,任務太多,超過maximumPoolSize了怎麼把?當然是接不下了,接不下那只有拒絕了。拒絕的時候可以指定拒絕策略,也就是一段處理程式。

決絕策略的父介面是RejectedExecutionHandler,JDK本身在ThreadPoolExecutor裡給使用者提供了四種拒絕策略,看一下:

1、AbortPolicy

直接丟擲一個RejectedExecutionException,這也是JDK預設的拒絕策略

2、CallerRunsPolicy

嘗試直接執行被拒絕的任務,如果執行緒池已經被關閉了,任務就被丟棄了

3、DiscardOldestPolicy

移除最晚的那個沒有被處理的任務,然後執行被拒絕的任務。同樣,如果執行緒池已經被關閉了,任務就被丟棄了

4、DiscardPolicy

不能執行的任務將被刪除

相關文章