線程池核心原理分析

一、基礎概念

線程池是一種多線程開發的處理方式,線程池可以方便得對線程進行創建,執行、銷燬和管理等操作。主要用來解決需要異步或併發執行任務的程序

談談池化技術

簡單點來說,就是預先保存好大量的資源,這些是可複用的資源,你需要的時候給你。對於線程,內存,oracle的連接對象等等,這些都是資源,程序中當你創建一個線程或者在堆上申請一塊內存時,都涉及到很多系統調用,也是非常消耗CPU的,如果你的程序需要很多類似的工作線程或者需要頻繁的申請釋放小塊內存,如果沒有在這方面進行優化,那很有可能這部分代碼將會成爲影響你整個程序性能的瓶頸。池化技術主要有線程池,內存池,連接池,對象池等等,對象池就是提前創建很多對象,將用過的對象保存起來,等下一次需要這種對象的時候,再拿出來重複使用。

線程池解決的問題:

1.線程池未出現前:如果併發的線程數量很多,並且每個線程都是執行一個時間很短的任務就結束了,這樣頻繁創建線程就會大大降低系統的效率,因爲頻繁創建線程和銷燬線程需要時間,而且會消耗系統資源。
如果使用線程池:線程在run()方法執行完後,不用將其銷燬,讓它繼續保持空閒狀態,當有新任務時讓它繼續執行新的任務。最後統一交給線程池來銷燬線程。
2.io操作過多或者比較耗時
3.主線程和子線程解耦

以下摘自《Java併發編程的藝術》
第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,
還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。但是,要做到合理利用
線程池,必須對其實現原理了如指掌。

二、線程池的實現原理

當向線程池提交一個任務之後,線程池是如何處理這個任務的呢?下圖就展示了線程池對任務的處理流程。

這裏寫圖片描述

從圖中可以看出,當提交一個新任務到線程池時,線程池的處理流程如下。
1.線程池判斷核心線程池裏的線程是否都在執行任務。如果不是,則創建一個新的工作
線程來執行任務。如果核心線程池裏的線程都在執行任務,則進入下個流程。
如核心線程池的容量爲5個線程。
1)如果有3個線程在工作,另外有2個線程沒有創建或者處於空閒狀態,那麼線程池就創建一個線程或者讓空閒狀態的線程來執行任務。
2)如果有5個線程都在工作,則進入下個流程。

2.線程池判斷工作隊列是否已經滿。如果工作隊列沒有滿,則將新提交的任務存儲在這
個工作隊列裏(期間不會創建新的線程)。如果工作隊列滿了,則進入下個流程。

3.線程池判斷線程池的線程是否都處於工作狀態。如果沒有,則創建一個新的工作線程
來執行任務。如果已經滿了,則交給飽和策略來處理這個任務。
問題:
直接創建一個新的工作線程來執行任務嗎?
答:是的,不會使用核心線程池裏的線程,任務執行完後,這個線程的生命週期由所設置的keepAliveTime的大小控制。

名詞解釋:

工作線程:線程池創建線程時,會將線程封裝成工作線程Worker,Worker在執行完任務
後,還會循環獲取工作隊列裏的任務來執行。

三、線程池中線程的執行流程

ThreadPoolExecutor執行execute()方法的示意圖,如圖9-2所示。

這裏寫圖片描述

ThreadPoolExecutor執行execute方法分下面4種情況。

1.如果當前運行的線程少於corePoolSize,則創建新線程來執行任務

2.如果運行的線程等於或多於corePoolSize,則將任務加入BlockingQueue。
線程池會讓corePoolSize裏執行完任務的線程反覆的獲取BlockingQueue的任務執行。

3.如果無法將任務加入BlockingQueue(隊列已滿),則創建新的線程來處理任務

4.如果創建新線程將使當前運行的線程超出maximumPoolSize,任務將被拒絕,並調用
RejectedExecutionHandler.rejectedExecution()方法。

問題:
1).全局鎖

四、線程池中的各個組件

4.1 ThreadPoolExecutor類

ThreadPoolExecutor類的層級結構如下:

這裏寫圖片描述

Executor接口

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

Executor接口只有一個方法execute(),通過這個方法可以向線程池提交一個任務,交由線程池去執行

ExecutorService接口

1.submit():

提交一個返回值的任務用於執行,返回一個表示任務的未決結果的 Future。

submit()和execute()的區別

1.最大的區別是submit()可以有返回值


2.submit()裏面實際上也會執行execute()方法,,只不過它利用了Future來獲取任務執行結果。而execute沒有返回結果

2.shutdown()

啓動一次順序關閉,執行以前提交的任務,但不接受新任務。

ThreadPoolExecutor類
在ThreadPoolExecutor類中提供了四個構造方法:

public class ThreadPoolExecutor extends AbstractExecutorService {
    .....
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
            BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler);
 
    public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,
        BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler);
    ...
}

從上面的代碼可以得知,ThreadPoolExecutor繼承了AbstractExecutorService類,並提供了四個構造器,事實上,通過觀察每個構造器的源碼具體實現,發現前面三個構造器都是調用的第四個構造器進行的初始化工作。

各個參數名詞解釋:

corePoolSize(線程池的基本大小):核心池的大小。當提交一個任務到線程池時,線程池會創建一個線
程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任
務數大於線程池基本大小時就不再創建。如果調用了線程池的prestartAllCoreThreads()方法,
線程池會提前創建並啓動所有基本線程。

maximumPoolSize:線程池最大線程數。線程池允許創建的最大線程數。如果隊列滿了,並
且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。值得注意的是,如
果使用了無界的任務隊列這個參數就沒什麼效果

keepAliveTime:表示線程沒有任務執行時最多保持多久時間會終止。默認情況下,只有當線程池中的線程數大於corePoolSize時,keepAliveTime纔會起作用,直到線程池中的線程數不大於corePoolSize,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高線程的利用率。

unit:參數keepAliveTime的時間單位。

workQueue:用於保存等待執行的任務的阻塞隊列.。這個參數的選擇也很重要,會對線程池的運行過程產生重大影響可以選擇以下幾個阻塞隊列。

·ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,初始化時必須指定其大小。
此隊列按FIFO(先進先出)原則對元素進行排序。內部通過ReentrantLock 來保證併發的安全性
·LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,LinkedBlockingQueue的容量爲Integer.MAX_VALUE即2^31-1.此隊列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列。
·SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用
移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於Linked-BlockingQueue,靜態工
廠方法Executors.newCachedThreadPool使用了這個隊列。
·PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。

ArrayBlockingQueue和PriorityBlockingQueue使用較少,一般使用LinkedBlockingQueue和Synchronous。線程池的排隊策略與BlockingQueue有關。

SynchronousQueue詳解

作爲BlockingQueue中的一員,SynchronousQueue與其他BlockingQueue有着不同特性:

1.SynchronousQueue沒有容量。與其他BlockingQueue不同,SynchronousQueue是一個不存儲元素的BlockingQueue。每一個put操作必須要等待一個take操作,否則不能繼續添加元素,反之亦然。
2.因爲沒有容量,所以對應 peek, contains, clear, isEmpty … 等方法其實是無效的。例如clear是不執行任何操作的,contains始終返回false,peek始終返回null。
3.SynchronousQueue分爲公平和非公平,默認情況下采用非公平性訪問策略,當然也可以通過構造函數來設置爲公平性訪問策略(爲true即可)。
4.若使用 TransferQueue, 則隊列中永遠會存在一個 dummy node(這點後面詳細闡述)

threadFactory:線程工廠,主要用來創建線程;

handler:表示當拒絕處理任務時的策略。

各個參數的詳細解釋請參考: http://www.cnblogs.com/dolphin0520/p/3932921.html

合理地配置線程池

要想合理地配置線程池,就必須首先分析任務特性,可以從以下幾個角度來分析。
·任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
·任務的優先級:高、中和低。
·任務的執行時間:長、中和短。
·任務的依賴性:是否依賴其他系統資源,如數據庫連接。

性質不同的任務可以用不同規模的線程池分開處理。CPU密集型任務應配置儘可能小的線程,如配置Ncpu+1個線程的線程池。由於IO密集型任務線程並不是一直在執行任務,則應配儘可能多的線程,如2*Ncpu。混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。可以通過Runtime.getRuntime().availableProcessors()方法獲得當前設備的CPU個數。

優先級不同的任務可以使用優先級隊列PriorityBlockingQueue來處理。它可以讓優先級高的任務先執行。

注意 如果一直有優先級高的任務提交到隊列裏,那麼優先級低的任務可能永遠不能執行。

執行時間不同的任務可以交給不同規模的線程池來處理,或者可以使用優先級隊列,讓執行時間短的任務先執行。

依賴數據庫連接池的任務,因爲線程提交SQL後需要等待數據庫返回結果,等待的時間越長,則CPU空閒時間就越長,那麼線程數應該設置得越大,這樣才能更好地利用CPU。

建議使用有界隊列:有界隊列能增加系統的穩定性和預警能力,可以根據需要設大一點兒,比如幾千。如果設置成無界隊列可能會撐爆。如依賴於數據庫的線程,當數據庫發生異常時,其他線程將不斷進入阻塞隊列,可能會撐爆jvm內存空間,導致整個系統不可用。

Executors
一般如果對線程池沒有特別深入的研究或特別複雜的業務,不建議開發人員自己手動配置線程池。如果要手動配置線程池可以使用spring提供ThreadPoolTaskExecutor類進行實現。

java中的Executors提供了很多靜態工廠來配置線程池如下所示(圖片來源於core java):

這裏寫圖片描述

推薦使用Executors.newCachedThreadPool():
1.Executors.newCachedThreadPool():
其源碼如下:

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

創建一個核心數爲0,最大數爲Integer.MAX_VALUE即(2^31)-1。被創建的線程60秒沒有任務的時候就會被回收。由於採用的是SynchronousQueue(一個不存儲元素的阻塞隊列),當任務超過核心數的時候,就會創建線程去執行任務。這意味着,如果主線程提交任務的速度高於maximumPool中線程處理任務的速度時,CachedThreadPool會不斷創建新線程。極端情況下,CachedThreadPool會因爲創建過多線程而耗盡CPU和內存資源。偏向於需要較多線程的業務,即cup空閒多需提升cup利用率的業務。主線程提交的任務需要及時執行的場景。
CachedThreadPool的實現原理如圖:
在這裏插入圖片描述

對圖10-6的說明如下。
1)首先執行SynchronousQueue.offer(Runnable task)。如果當前maximumPool中有空閒線程
正在執行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那麼主線程執行
offer操作與空閒線程執行的poll操作配對成功,主線程把任務交給空閒線程執行,execute()方
法執行完成;否則執行下面的步驟2)。
2)當初始maximumPool爲空,或者maximumPool中當前沒有空閒線程時,將沒有線程執行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這種情況下,步驟1)將失
敗。此時CachedThreadPool會創建一個新線程執行任務,execute()方法執行完成。
3)在步驟2)中新創建的線程將任務執行完後,會執行
SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。這個poll操作會讓空閒線程最多在SynchronousQueue中等待60秒鐘。如果60秒鐘內主線程提交了一個新任務(主線程執
行步驟1)),那麼這個空閒線程將執行主線程提交的新任務;否則,這個空閒線程將終止。由於
空閒60秒的空閒線程會被終止,因此長時間保持空閒的CachedThreadPool不會使用任何資源。

2.Executors.newFixedThreadPool()
固定線程池:

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

newFixedThreadPool 創建一個固定核心數和最大線程數的線程池,被創建的線程將不會被回收,超出的線程會在基於鏈表的阻塞隊列中等待。偏向於控制線程數的業務,即需要較少線程的業務。

3.Executors.newSingleThreadExecutor
單線程線程池。其源碼如下:

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

單線程線程池,核心線程數和最大線程數均爲1,空閒線程存活0毫秒同樣無意思,意味着每次只有一個線程執行任務,多餘的先存儲到工作隊列,一個一個執行,保證了線程的順序執行。

4.Executors.newScheduledThreadPool
調度線程池。其源碼如下:

 public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

 public ScheduledThreadPoolExecutor(int corePoolSize,
                                       ThreadFactory threadFactory) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue(), threadFactory);
    }

即按一定的週期執行任務,即定時任務,對ThreadPoolExecutor進行了包裝而已。

如何提交線程

如可以先隨便定義一個固定大小的線程池

ExecutorService es = Executors.newFixedThreadPool(3);



提交一個線程

es.submit(xxRunnble);

es.execute(xxRunnble);


如何關閉線程池

es.shutdown(); 
不再接受新的任務,之前提交的任務等執行結束再關閉線程池。

es.shutdownNow();
不再接受新的任務,試圖停止池中的任務再關閉線程池,返回所有未處理的線程list列表。

4.2 Future,FutureTask

Future:Future 表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並獲取計算的結果。
1)多用於獲取callable的返回結果

FutureTask:對所繼承的接口進行了基本的實現。
其對應關係如下

這裏寫圖片描述

三、Java中的併發工具類

CountDownLatch

CountDownLatch類位於java.util.concurrent包下,利用它可以實現類似計數器的功能。比如有一個任務A,它要等待其他4個任務執行完畢之後才能執行,此時就可以利用CountDownLatch來實現這種功能了。

CountDownLatch類只提供了一個構造器如下:

 /**
     * Constructs a {@code CountDownLatch} initialized with the given count.
     *
     * @param count the number of times {@link #countDown} must be invoked
     *        before threads can pass through {@link #await}
     * @throws IllegalArgumentException if {@code count} is negative
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }

然後下面這3個方法是CountDownLatch類中最重要的方法:

public void await() throws InterruptedException { };   //調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public boolean await(long timeout, TimeUnit unit) throws InterruptedException { };  //和await()類似,只不過等待一定的時間後count值還沒變爲0的話就會繼續執行
public void countDown() { };  //將count值減1

注意與join()方法的聯繫

參考資料
1.《Java併發編程的藝術》方騰飛 魏鵬 程曉明 著
2.《core java》
3. Java併發編程:線程池的使用:http://www.cnblogs.com/dolphin0520/p/3932921.html
4. 【死磕Java併發】—– 死磕 Java 併發精品合集http://cmsblogs.com/?p=2611
5. java高級應用:線程池全面解析https://mp.weixin.qq.com/s/fFZfEe10bdVKBndrEFH4fA

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章