走進Java Android 的執行緒世界(二)執行緒池

Tongson發表於2018-12-04

上集回顧: 上集我們稍做講解了Future與Executor

Executors

建立並得到ThreadPoolExecutor的工廠。

ThreadPoolExecutor執行示意圖.png

ThreadPoolExecutor

執行緒池的真正實現

提供構造方法

構造方法.png

#建立一個執行緒池時需要輸入幾個引數,如下建立一個執行緒池時需要輸入幾個引數,如下

corePoolSize

執行緒池的核心執行緒數,預設情況下,核心執行緒會線上程池中一直存活,即使它們處於閒置狀態。如果將ThreadPoolExecutor中的allowCoreThreadTimeOut屬性設定為true,那麼閒置的核心執行緒在等待新任務到來時會有超時策略,這個時間間隔由keepAliveTime所指定,當等待時間超過keepAliveTime所指定的時長後,核心執行緒就會被終止。

執行緒池中的核心執行緒數,當提交一個任務時,執行緒池建立一個新執行緒執行任務,直到當前執行緒數等於corePoolSize;如果當前執行緒數為corePoolSize,繼續提交的任務被儲存到阻塞佇列中,等待被執行;如果執行了執行緒池的prestartAllCoreThreads()方法,執行緒池會提前建立並啟動所有核心執行緒。

maximumPoolSize

執行緒池所能容納的最大執行緒數,當活動執行緒數達到這個數值後,後續的新任務將會被阻塞。

執行緒池中允許的最大執行緒數。如果當前阻塞佇列滿了,且繼續提交任務,則建立新的執行緒執行任務,前提是當前執行緒數小於maximumPoolSize。

keepAliveTime

非核心執行緒閒置時的超時時長,超過這個時長,非核心執行緒就會被回收。當ThreadPoolExecutor的allowCoreThreadTimeOut屬性設定為true時,keepAliveTime同樣會作用於核心執行緒。

執行緒空閒時的存活時間,即當執行緒沒有任務執行時,繼續存活的時間;預設情況下,該引數只線上程數大於corePoolSize時才有用。

unit

用於指定keepAliveTime引數的時間單位,這是一個列舉,常用的有TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)以及TimeUnit.MINUTES(分鐘)等。

keepAliveTime的單位。

BlockingQueue:用來暫時儲存任務的工作佇列。

執行緒池中的任務佇列,通過執行緒池的execute方法提交的Runnable物件會儲存在這個引數中。

維護著等待執行的Runnable物件。

當所有的核心執行緒都在幹活時,新新增的任務會被新增到這個佇列中等待處理,如果佇列滿了,則新建非核心執行緒執行任務

用來儲存等待被執行的任務的阻塞佇列,且任務必須實現Runable介面,在JDK中提供瞭如下阻塞佇列:

1、ArrayBlockingQueue:

基於陣列結構的有界阻塞佇列,按FIFO排序任務;

可以限定佇列的長度,接收到任務的時候,如果沒有達到corePoolSize的值,則新建執行緒(核心執行緒)執行任務,如果達到了,則入隊等候,如果佇列已滿,則新建執行緒(非核心執行緒)執行任務,又如果匯流排程數到了 maximumPoolSize,並且佇列也滿了,則發生錯誤。

作用與應用:幫助限制資源的消耗,但是不容易控制。佇列長度和maximumPoolSize這兩個值會相互影響,使用大的佇列和小maximumPoolSize會減少CPU的使用、作業系統資源、上下文切換的消耗,但是會降低吞吐量,如果任務被頻繁的阻塞如IO執行緒,系統其實可以排程更多的執行緒。使用小的佇列通常需要大maximumPoolSize,從而使得CPU更忙一些,但是又會增加降低吞吐量的執行緒排程的消耗。總結一下是 IO密集型可以考慮多些執行緒來平衡CPU的使用,CPU密集型可以考慮少些執行緒減少執行緒排程的消耗。

2、LinkedBlockingQuene:

基於連結串列結構的阻塞佇列,按FIFO排序任務,吞吐量通常要高於ArrayBlockingQuene;

這個佇列接收到任務的時候,如果當前執行緒數小於核心執行緒數,則新建執行緒(核心執行緒)處理任務;如果當前執行緒數等於核心執行緒數,則進入佇列等待。由於這個佇列沒有最大值限制,即所有超過核心執行緒數的任務都將被新增到佇列中,這也就導致了 maximumPoolSize 的設定失效,因為匯流排程數永遠不會超過 corePoolSize。

作用與應用:沒有指定最大容量的時候,將會引起當核心執行緒都在忙的時候,新的任務被放在佇列上,因此,永遠不會有大於corePoolSize的執行緒被建立,因此maximumPoolSize引數將失效。這種策略比較適合所有的任務都不相互依賴,獨立執行。舉個例子,如網頁伺服器中,每個執行緒獨立處理請求。但是當任務處理速度小於任務進入速度的時候會引起佇列的無限膨脹。

3、SynchronousQuene:

一個不儲存元素的阻塞佇列,每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQuene;

注意:這個佇列接收到任務的時候,會直接提交給執行緒處理,而不保留它,如果所有執行緒都在工作怎麼辦?那就新建一個執行緒來處理這個任務!所以為了保證不出現<執行緒數達到了 maximumPoolSize 而不能新建執行緒>的錯誤,使用這個型別佇列的時候,maximumPoolSize 一般指定成 Integer.MAX_VALUE,即無限大。

作用與應用:會將提交的任務直接傳送給工作執行緒,而不持有。如果當前沒有工作執行緒來處理,即任務放入佇列失敗,則根據執行緒池的實現,會引發新的工作執行緒建立,因此新提交的任務會被處理。這種策略在當提交的一批任務之間有依賴關係的時候避免了鎖競爭消耗。值得一提的是,這種策略最好是配合unbounded執行緒數來使用,從而避免任務被拒絕。同時我們必須要考慮到一種場景,當任務到來的速度大於任務處理的速度,將會引起無限制的執行緒數不斷的增加。

4、priorityBlockingQuene:

具有優先順序的無界阻塞佇列;

5、DelayQueue:

佇列內元素必須實現 Delayed 介面,這就意味著你傳進去的任務必須先實現 Delayed 介面。這個佇列接收到任務時,首先先入隊,只有達到了指定的延時時間,才會執行任務。

DelayQueue封裝了一個PriorityQueue,這個PriorityQueue會對佇列中的ScheduledFutureTask進行排序。排序時,time小的排在前面(時間早的任務將被先執行)。如果兩個 ScheduledFutureTask的time相同,就比較sequenceNumber,sequenceNumber小的排在前面(也就是說,如果兩個任務的執行時間相同,那麼先提交的任務將被先執行)。

threadFactory

執行緒工廠,為執行緒池提供建立新執行緒的功能。ThreadFactory是一個介面,它只有一個方法:Thread newThread(Runnable r)。

RejectedExecutionHandler(飽和策略):

當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略預設情況下是AbortPolicy,表示無法 處理新任務時丟擲異常除了上面的這些主要引數外,ThreadPoolExecutor還有一個不常用的引數RejectedExecutionHandler handler。當執行緒池無法執行新的任務時,這可能是猶豫任務佇列已滿或者是無法成功執行任務,這個時候ThreadPoolExecutor會呼叫handler的RejectedExecutionException。ThreadPoolExecutor為RejectedExecutionHandler 提供了幾個可選值:CallerRunsPolicy、AbortPolicy、DiscardPolicy和DiscardOldestPolicy,其中AbortPolicy是預設值,它會直接丟擲RejectedExecutionException。

執行緒池的飽和策略,當阻塞佇列滿了,且沒有空閒的工作執行緒,如果繼續提交任務,必須採取一種策略處理該任務,執行緒池提供了4種策略:

1、AbortPolicy:直接丟擲異常,預設策略;

2、CallerRunsPolicy:用呼叫者所在的執行緒來執行任務;

3、DiscardOldestPolicy:丟棄阻塞佇列中靠最前的任務,並執行當前任務;

4、DiscardPolicy:直接丟棄任務;

當然也可以根據應用場景實現RejectedExecutionHandler介面,自定義飽和策略,如記錄日誌或持久化儲存不能處理的任務。

ThreadPoolExecutor執行任務時大致遵循如下規則:

(1)如果執行緒池中的執行緒數量未達到核心執行緒的數量,那麼會直接啟動核心執行緒來執行任務。

(2)如果執行緒池中的執行緒數量已經達到或者超過核心執行緒的數量,那麼任務會被出入到佇列中等待執行。

(3)如果在步驟2中無法將任務插入到任務佇列中,這往往是由於任務佇列已滿,這個時候如果執行緒數量未大道執行緒池規定的最大值,那麼會立即啟動一個非核心執行緒來執行任務。

(4)如果步驟3中執行緒數量已經達到執行緒池規定的最大值,那麼就拒絕執行此任務,ThreadPoolExecutor會呼叫RejectedExecutionHandler的RejectedExecution方法來通知呼叫者。

執行緒池的主要處理流程.png

執行緒池解決的兩個問題:

1)執行緒池通過減少每次做任務的時候產生的效能消耗來優化執行大量的非同步任務的時候的系統效能。

2)執行緒池還提供了限制和管理批量任務被執行的時候消耗的資源、執行緒的方法。

另外ThreadPoolExecutor還提供了簡單的統計功能,比如當前有多少任務被執行完了。

執行緒池的分類

1.FixedThreadPool

執行緒數量固定,當執行緒處於空閒狀態時,不會被回收。

只有沒有超時機制的核心執行緒。

任務佇列並沒有大小限制。(超出的執行緒會在佇列中等待)

用的是LinkedBlockingQuene佇列。

2.CachedThreadPool

只有非核心執行緒,並且最大執行緒數為Integer.MAX_VALUE(很大的數0x7fffffff)。

適用於執行量大,耗時較少的任務。

用的是SynchronousQuene佇列。

3.ScheduledThreadPool

核心執行緒數量是固定的。

非核心執行緒數量沒有限制。

當非核心執行緒限制時會被立即回收。

適用於執行定時任務和具有固定週期的重複任務。

用的是DelayQueue佇列。

延遲啟動任務schedule(Runnable command,long delay,TimeUnit unit)

延遲定時執行任務scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit)

延遲執行任務scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit)

4.SingleThreadExecutor

只有一個核心執行緒,確保所有任務都在同一個執行緒中按順序執行。

意義在於統一所有外界任務到一個執行緒中,使得在這些任務之間不需要處理執行緒同步的問題。

用的是LinkedBlockingQuene佇列。

ThreadPoolExecutor FixedThreadPool CachedThreadPool ScheduledThreadPool SingleThreadExecutor
corePoolSize nThreads 0 corePoolSize 1
maximumPoolSize nThreads Integer.MAX_VALUE Integer.MAX_VALUE 1
keepAliveTime 0L 60L DEFAULT_KEEPALIVE_MILLIS 0L
unit TimeUnit.MILLISECONDS TimeUnit.SECONDS MILLISECONDS TimeUnit.MILLISECONDS
workQueue new LinkedBlockingQueue() new SynchronousQueue() new DelayedWorkQueue() new LinkedBlockingQueue()

FixedThreadPool.png

CachedThreadPool.png

ScheduledThreadPoolExecutor.png

SingleThreadExecutor.png

執行緒池其他常用方法

向執行緒池提交任務

execute()

submit()

一般情況下我們使用execute來提交任務,但是有時候可能也會用到submit,使用submit的好處是submit有返回值。

submit方法.png

關閉執行緒

shutDown() 關閉執行緒池,不影響已經提交的任務。

shutDownNow() 關閉執行緒池,並嘗試去終止正在執行的執行緒。

allowCoreThreadTimeOut(boolean value) 允許核心執行緒閒置超時時被回收。

更深入分析ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor的執行主要分為兩大部分。

1)當呼叫ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法時,會向ScheduledThreadPoolExecutor的DelayQueue新增一個實現了 RunnableScheduledFutur介面的ScheduledFutureTask。

2)執行緒池中的執行緒從DelayQueue中獲取ScheduledFutureTask,然後執行任務。

ScheduledThreadPoolExecutor為了實現週期性的執行任務,對ThreadPoolExecutor做了如下的修改。

· 使用DelayQueue作為任務佇列。

· 獲取任務的方式不同(後文會說明)。

· 執行週期任務後,增加了額外的處理(後文會說明)。

前面我們提到過,ScheduledThreadPoolExecutor會把待排程的任務(ScheduledFutureTask) 放到一個DelayQueue中。

ScheduledFutureTask主要包含3個成員變數,如下。

·long型成員變數time,表示這個任務將要被執行的具體時間。

·long型成員變數sequenceNumber,表示這個任務被新增到ScheduledThreadPoolExecutor中的序號。

·long型成員變數period,表示任務執行的間隔週期。

DelayQueue封裝了一個PriorityQueue,這個PriorityQueue會對佇列中的ScheduledFutureTask進行排序。排序時,time小的排在前面(時間早的任務將被先執行)。

如果兩個 ScheduledFutureTask的time相同,就比較sequenceNumber,sequenceNumber小的排在前面(也就是說,如果兩個任務的執行時間相同,那麼先提交的任務將被先執行)。

首先,讓我們看看ScheduledThreadPoolExecutor中的執行緒執行週期任務的過程

圖是 ScheduledThreadPoolExecutor中的執行緒1執行某個週期任務的4個步驟。

ScheduledThreadPoolExecutor的任務執行步驟.png
下面是對這4個步驟的說明。

1)執行緒1從DelayQueue中獲取已到期的ScheduledFutureTask(DelayQueue.take())。到期任務是指ScheduledFutureTask的time大於等於當前時間。

2)執行緒1執行這個ScheduledFutureTask。

3)執行緒1修改ScheduledFutureTask的time變數為下次將要被執行的時間。

4)執行緒1把這個修改time之後的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

接下來,讓我們看看上面的步驟

1)獲取任務的過程。下面是 DelayQueue.take() 方法的原始碼實現。

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();// 1 
        try {
            for (; ; ) {
                E first = q.peek();
                if (first == null) {
                    available.await();// 2.1 
                } else {
                    long delay = first.getDelay(TimeUnit.NANOSECONDS);
                    if (delay > 0) {
                        long tl = available.awaitNanos(delay);// 2.2  
                    } else {
                        E x = q.poll();// 2.3.1    
                        assert x != null;
                        if (q.size() != 0) available.signalAll();   // 2.3.2   
                        return x;
                    }
                }
            }
        } finally {
            lock.unlock();// 3 
        }
    }
複製程式碼

DelayQueue.take()的執行示意圖

ScheduledThreadPoolExecutor獲取任務的過程.png

如圖所示,獲取任務分為3大步驟。

1)獲取Lock。

2)獲取週期任務。

·如果PriorityQueue為空,當前執行緒到Condition中等待;否則執行下面的2.2。

·如果PriorityQueue的頭元素的time時間比當前時間大,到Condition中等待到time時間;否 則執行下面的2.3。

·獲取PriorityQueue的頭元素(2.3.1);如果PriorityQueue不為空,則喚醒在Condition中等待的所有執行緒(2.3.2)。

3)釋放Lock。

ScheduledThreadPoolExecutor在一個迴圈中執行步驟2,直到執行緒從PriorityQueue獲取到一個元素之後(執行2.3.1之後),才會退出無限迴圈(結束步驟2)。

最後,讓我們看看ScheduledThreadPoolExecutor中的執行緒執行任務的步驟4,把ScheduledFutureTask放入DelayQueue中的過程。

下面是 DelayQueue.add() 的原始碼實現。

    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();// 1
        try {
            E first = q.peek();
            q.offer(e);// 2.1        
            if (first == null || e.compareTo(first) < 0) available.signalAll();// 2.2
            return true;
        } finally {
            lock.unlock();// 3  
        }
    }
複製程式碼

DelayQueue.add()的執行示意圖:

ScheduledThreadPoolExecutor新增任務的過程.png

如圖所示,新增任務分為3大步驟。

1)獲取Lock。

2)新增任務。

·向PriorityQueue新增任務。

·如果在上面2.1中新增的任務是PriorityQueue的頭元素,喚醒在Condition中等待的所有執行緒。

3)釋放Lock。

BlockingQueue

Worker

自定義執行緒池

相關文章