大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

Anlia發表於2018-02-12

版權宣告:本文為博主原創文章,未經博主允許不得轉載
原始碼:github.com/AnliaLee
大家要是看到有錯誤的地方或者有啥好的建議,歡迎留言評論

前言

本篇部落格我們將開始探索由上一章引出的執行緒池ThreadPoolExecutor)的知識。由於內含大量示例,導致文章篇幅有點長,望大家耐心食用...

往期回顧
大話Android多執行緒(一) Thread和Runnable的聯絡和區別
大話Android多執行緒(二) synchronized使用解析
大話Android多執行緒(三) 執行緒間的通訊機制之Handler
大話Android多執行緒(四) Callable、Future和FutureTask


ThreadPoolExecutor簡介

簡介這東西也寫不出啥花樣來,遂直接偷懶引用別人的吧哈哈

為什麼要引入執行緒池?

new Thread()的缺點
• 每次new Thread()耗費效能
• 呼叫new Thread()建立的執行緒缺乏管理,被稱為野執行緒,而且可以無限制建立,之間相互競爭,會導致過多佔用系統資源導致系統癱瘓
• 不利於擴充套件,比如如定時執行、定期執行、執行緒中斷

採用執行緒池的優點
• 重用存在的執行緒,減少物件建立、消亡的開銷,效能佳
• 可有效控制最大併發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞
• 提供定時執行、定期執行、單執行緒、併發數控制等功能

以上內容摘自Android執行緒管理之ExecutorService執行緒池

執行緒池ThreadPoolExecutor的繼承關係如下圖所示

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

下一節我們將介紹ThreadPoolExecutor的構造引數


引數解析

構造ThreadPoolExecutor時需傳入許多引數,我們以引數最多的那個構造方法為例(因為引數threadFactoryhandler是有預設值的,所以和其他幾個構造方法的區別只是有無設定這兩個引數的入口而已,就不贅述了)

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

我們將引數分析代入到程式設計師開發的故事中,讓大家更容易理解,以下是引數介紹

  • int corePoolSize
    計劃招聘核心程式設計師的數量。核心程式設計師是公司的頂樑柱,公司接到甲方需求(即任務)後會優先分配給核心程式設計師去開發

    執行緒池中核心執行緒的數量

  • int maximumPoolSize
    計劃招聘程式設計師的總數。程式設計師由核心程式設計師實習生組成

    執行緒池中執行緒數的最大值

  • long keepAliveTime
    允許員工打醬油的時間。公司招了實習生之後,如果發現一段時間(keepAliveTime)內實習生沒活幹,在那偷懶刷什麼掘金沸點的時候,就會把他辭掉。當然核心程式設計師抱著的也不一定是鐵飯碗,若公司採取了節省成本的經營策略(ThreadPoolExecutor.allowCoreThreadTimeOut設為true),核心程式設計師一段時間沒活幹也一樣會被裁員

    執行緒的閒置時長,預設情況下此引數只作用於非核心執行緒,即非核心執行緒閒置時間超過keepAliveTime後就會被回收。但如果ThreadPoolExecutor.allowCoreThreadTimeOut設為true,則引數同樣可以作用於核心執行緒

  • TimeUnit unit
    上面時間引數的單位,有納秒、微秒、毫秒、秒、分、時、天

    可供選擇的單位型別有:
    TimeUnit.NANOSECONDS:納秒
    TimeUnit.MICROSECONDS:微秒
    TimeUnit.MILLISECONDS:毫秒
    TimeUnit.SECONDS:秒
    TimeUnit.MINUTES:分
    TimeUnit.HOURS:小時
    TimeUnit.DAYS:天

  • BlockingQueue<Runnable> workQueue
    儲備任務的佇列

    執行緒池中的任務佇列,該佇列主要用來儲存已經提交但尚未分配給執行緒執行的任務。BlockingQueue,即阻塞佇列,可供傳入的佇列型別有:
    ArrayBlockingQueue:基於陣列的阻塞佇列
    LinkedBlockingQueue:基於連結串列的阻塞佇列
    PriorityBlockingQueue:基於優先順序的阻塞佇列
    DelayQueue:基於延遲時間優先順序的阻塞佇列
    SynchronousQueue:基於同步的阻塞佇列

    我們在下面的章節中將會詳細對比以上這幾種佇列的區別。此外,還需注意傳入任務的都需實現Runnable介面

  • ThreadFactory threadFactory

    執行緒工廠介面,只有一個new Thread(Runnable r)方法,可以為執行緒池建立新執行緒。系統為我們提供了預設的threadFactory:Executors.defaultThreadFactory(),我們一般使用預設的就可以了

  • RejectedExecutionHandler handler

    拒絕策略,預設使用ThreadPoolExecutor.AbortPolicy,當新任務被拒絕時會將丟擲RejectExecutorException異常。此外還有3種策略可供選擇:CallerRunsPolicyDiscardPolicyDiscardOldestPolicy


ThreadPoolExecutor的任務執行策略

當我們使用submit或者execute方法將任務提交到執行緒池時,執行緒池遵循以下策略將任務分配給相應執行緒去執行(任務佇列使用最基本的ArrayBlockingQueue

  • HR根據任務執行情況來決定何時招核心程式設計師,如果接到一個需求後發現核心程式設計師手上都有任務(或者一個程式設計師都沒有的時候),就會招一個進來,招滿為止

    提交任務後,如果執行緒池中的執行緒數未達到核心執行緒的數量(corePoolSize),則會建立一個核心執行緒去執行

舉個栗子,我們設定任務數(taskSize)為3,核心執行緒數(corePoolSize)為5,則 taskSize < corePoolSize,執行緒池會建立3條核心執行緒去執行任務(假設每個任務都需要一定時間才能完成)

public class ExecutorTest {
    //省略部分程式碼...
    private static int taskSize = 3;//任務數
    private static int corePoolSize = 5;//核心執行緒的數量
    private static int maximumPoolSize = 20;//執行緒數的最大值
    private static int queueSize = 128;//可儲存的任務數

    public static class TestTask implements Runnable {
        public void run() {
            if (taskSize > 0) {
                try{
                    Thread.sleep(500);//模擬開發時間
                    System.out.println(getTime() + getName(Thread.currentThread().getName())
                            + " 完成一個開發任務,編號為t" + (taskSize--)
                    );
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String args[]){
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(
                        corePoolSize,
                        maximumPoolSize,
                        1,
                        TimeUnit.SECONDS,
                        new ArrayBlockingQueue<Runnable>(queueSize)
                );
        TestTask task;
        int size = taskSize;
        for (int i = 0; i < size; i++) {
            task = new TestTask();
            executor.execute(task);
        }
        executor.shutdown();
    }
}
複製程式碼

執行結果見下圖(請忽略任務編號,現在還沒用到)

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


  • 又有新的任務下來時,如果核心程式設計師有人空閒,就扔給他做;如果手上都有任務,則暫時儲存到任務儲備清單(workQueue)中,等到誰有空了再交給他做。當然這個佇列可儲存的任務數量有限制的

    提交任務後,如果執行緒池中的執行緒數已經達到核心執行緒的數量(corePoolSize),但任務佇列workQueue)中儲存的任務數未達到最大值,則將任務存入任務佇列中等待執行

我們將任務數設為10核心執行緒數設為3任務佇列的最大值設為7,此時將任務分配給核心執行緒後剛好可以填滿任務佇列

private static int taskSize = 10;//任務數
private static int corePoolSize = 3;//核心執行緒的數量
private static int maximumPoolSize = 10;//執行緒數的最大值
private static int queueSize = 7;//可儲存的任務數
複製程式碼

執行結果見下圖

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


  • 核心組程式設計師手上的任務和儲備的任務(workQueue)都達到飽和時,會招聘一個實習生非核心執行緒)來分擔任務

    提交任務後,如果執行緒池中的執行緒數達到核心執行緒數未超過執行緒數的最大值,同時任務佇列中的任務數已達到最大值,則建立一個非核心執行緒來執行任務

我們將之前的任務數改為12,其他數值不變,那麼將會有兩位實習生參與到開發中(taskSize - (corePoolSize + queueSize) = 2

private static int taskSize = 12;//任務數
private static int corePoolSize = 3;//核心執行緒的數量
private static int maximumPoolSize = 10;//執行緒數的最大值
private static int queueSize = 7;//可儲存的任務數
複製程式碼

執行結果見下圖(多了實習生4和5

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


  • 如果又客戶又提了新的需求,但是核心程式設計師和實習生都沒空,咋辦?老闆:“那還用問?加班唄!”

加...加班???

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

正確答案應該是推掉!拒絕!

提交任務後,若執行緒池中的執行緒數已達到最大值,且所有執行緒均在執行任務,任務佇列也飽和了,則拒絕執行該任務,並根據拒絕策略執行相應操作

上個例子中一共建立了5條執行緒(3核心執行緒2非核心執行緒),那麼這次我們只將執行緒數的最大值改為4,採用預設的拒絕策略

private static int taskSize = 12;//任務數
private static int corePoolSize = 3;//核心執行緒的數量
private static int maximumPoolSize = 4;//執行緒數的最大值
private static int queueSize = 7;//可儲存的任務數

public static void main(String args[]){
	//省略部分程式碼...
	for (int i = 0; i < size; i++) {
		executor.execute(task);
		System.out.println("接到任務 " + i);
	}
	executor.shutdown();
}
複製程式碼

執行結果見下圖,可以看見執行緒池只接收了11個任務(maximumPoolSize + queueSize = 11 ),在提交第12個任務後會丟擲RejectedExecutionException的異常

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解
大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

另外需要注意的是,如果我們在提交任務時丟擲了異常,那麼之後呼叫的shutdown()將變為無效程式碼,執行緒池將一直執行在主執行緒中無法關閉


  • 還有一種特殊情況,如果公司不打算招核心程式設計師接到的任務又比任務佇列的容量要少,這時公司為了節省開支就只會招一個實習生來完成開發任務

    corePoolSize = 0 的條件下,提交任務後,若任務佇列中的任務數仍未達到最大值,執行緒池只會建立一條非核心執行緒來執行任務

private static int taskSize = 9;//任務數
private static int corePoolSize = 0;//核心執行緒的數量
private static int maximumPoolSize = 5;//執行緒數的最大值
private static int queueSize = 10;//可儲存的任務數
複製程式碼

執行結果見下圖

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


各型別任務佇列(BlockingQueue)的區別

BlockingQueue是一個介面,它提供了3個新增元素方法:

  • offer:新增元素到佇列裡,新增成功返回true,新增失敗返回false
  • add:新增元素到佇列裡,新增成功返回true,由於容量滿了新增失敗會丟擲IllegalStateException異常(add方法內部實際上呼叫的是offer方法)
  • put:新增元素到佇列裡,如果容量滿了會阻塞直到容量不滿

3個刪除元素的方法:

  • poll:刪除佇列頭部元素,如果佇列為空,返回null。否則返回元素
  • remove:基於物件找到對應的元素,並刪除。刪除成功返回true,否則返回false
  • take:刪除佇列頭部元素,如果佇列為空,一直阻塞到佇列有元素並刪除

我們之前講到的5種型別的佇列實際上都是BlockingQueue的實現類,本篇部落格不會具體分析原始碼的實現,我們只對比它們使用上的區別:

ArrayBlockingQueue

基於陣列的阻塞佇列,ArrayBlockingQueue內部維護了一個由大小固定的陣列構成的資料緩衝佇列,陣列大小在佇列初始化時就需要指定。而儲存在ArrayBlockingQueue中的元素需按照FIFO(先進先出)的方式來進行存取。ArrayBlockingQueue的構造方法如下

ArrayBlockingQueue(int capacity)
ArrayBlockingQueue(int capacity, boolean fair)
ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c)
複製程式碼
  • int capacity佇列容量的大小,即可儲存元素個數的最大值
  • boolean fair指定訪問策略,如果為設為true,則按照FIFO的順序訪問阻塞佇列;若設為false,則訪問順序是不確定的
  • Collection<? extends E> c:設定初始化要包含的collection元素,並以collection迭代器的遍歷順序新增元素

我們這裡僅以一個引數的構造方法為例,使用ArrayBlockingQueue時的任務執行策略和具體使用示例見上一節,就不重複再講一次了


LinkedBlockingQueue

基於連結串列單向連結串列)的阻塞佇列,和ArrayBlockingQueue類似,其內部維護著一個由單向連結串列構成的資料緩衝佇列。區別於ArrayBlockingQueueLinkedBlockingQueue在初始化的時候可以不用設定容量大小,其預設大小為Integer.MAX_VALUE(即2的31次方-1,表示 int 型別能夠表示的最大值)。若設定了大小,則使用起來和ArrayBlockingQueue一樣。LinkedBlockingQueue的構造方法如下

LinkedBlockingQueue()
LinkedBlockingQueue(int capacity)
LinkedBlockingQueue(Collection<? extends E> c)
複製程式碼

這裡引數和之前講的一樣,而且使用方法和ArrayBlockingQueue大同小異,就不贅述了


PriorityBlockingQueue

基於優先順序的阻塞佇列,用法類似於LinkedBlockingQueue,區別在於其儲存的元素不是按照FIFO排序的,這些元素的排序規則得由我們自己來定義:所有插入PriorityBlockingQueue的物件元素必須實現Comparable自然排序)介面,我們對Comparable介面的實現定義了佇列優先順序排序規則

PriorityBlockingQueue的佇列容量是“無界”的,因為新任務進來時如果發現已經超過了佇列的初始容量,則會執行擴容的操作。這意味著如果corePoolSize > 0,執行緒池中的執行緒數達到核心執行緒數的最大值,且任務佇列中的任務數也達到最大值,這時新的任務提交進來,執行緒池並不會建立非核心執行緒來執行新任務,而是對任務佇列進行擴容

更加具體的內容大家可以去研究一下這篇篇部落格:併發佇列 – 無界阻塞優先順序佇列 PriorityBlockingQueue 原理探究

PriorityBlockingQueue的構造方法如下

PriorityBlockingQueue()
PriorityBlockingQueue(int initialCapacity)
PriorityBlockingQueue(int initialCapacity, Comparator<? super E> comparator)
PriorityBlockingQueue(Collection<? extends E> c)
複製程式碼

重複的引數就不解釋了

  • int initialCapacity:優先順序佇列的初始容量大小,預設大小為11DEFAULT_INITIAL_CAPACITY
  • Comparator<? super E> comparator:用於優先順序佇列排序的比較器比較器排序)。若此引數設為null,則佇列元素按我們之前實現Comparable自然排序)介面的排序規則進行排序,否則用comparator比較器排序)中定義的排序規則進行排序(實現Comparator的好處在於可以將比較排序演算法具體的實體類分離,更加詳細的對比大家可以自行查閱資料瞭解)

下面我們來看看具體示例(以最簡單的場景體驗一下使用過程): 我們將核心程式設計師的數量(corePoolSize)設為0任務佇列的容量使用預設的(11),這時公司接到了12個開發任務,專案經理會根據任務的優先順序對任務執行的順序進行排序,然後分配給實習生(公司只能招到一個實習生,原因我們在解析任務執行策略的時候已經講過了)

public class ExecutorTest {
    //省略部分程式碼...
    private static int taskSize = 9;//任務數
    private static int corePoolSize = 0;//核心執行緒的數量
    private static int maximumPoolSize = 5;//執行緒數的最大值
    private static int queueSize = 10;//可儲存的任務數

    public static class PriorityTask implements Runnable,Comparable<PriorityTask>{
        private int priority;

        public PriorityTask(int priority) {
            this.priority = priority;
        }
        @Override
        public void run() {
            if (taskSize > 0) {
                try{
                    Thread.sleep(1000);//模擬開發時間
                    System.out.println(getTime() + getName(Thread.currentThread().getName())
                            + " 完成一個開發任務,編號為t" + (taskSize--) + ", 優先順序為:" + priority
                    );
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        @Override
        public int compareTo(PriorityTask task) {
            if(this.priority == task.priority){
                return 0;
            }
            return this.priority<task.priority?1:-1;//優先順序大的先執行
        }
    }

    public static void main(String args[]){
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(
                        corePoolSize,
                        maximumPoolSize,
                        1,
                        TimeUnit.SECONDS,
                        new PriorityBlockingQueue<Runnable>(queueSize)
                );
				
        Random random = new Random();
        PriorityTask task;
        int size = taskSize;
        for (int i = 0; i < size; i++) {
            int p = random.nextInt(100);
            task = new PriorityTask(p);
            executor.execute(task);
            System.out.println("接到任務 " + i + ",優先順序為:" + p);
        }
        executor.shutdown();
    }
}
複製程式碼

執行結果見下圖

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


DelayQueue

基於延遲時間優先順序的阻塞佇列,DelayQueuePriorityBlockingQueue非常相似,同樣是“無界”佇列(DelayQueue不需要設定初始容量大小),同樣基於優先順序進行排序。但有一點不同,DelayQueue中的元素必須實現 Delayed介面(Delayed繼承自Comparable介面),我們需重寫Delayed.getDelay方法為元素的釋放(執行任務)設定延遲getDelay方法的返回值是佇列元素被釋放前的保持時間,如果返回0一個負值,就意味著該元素已經到期需要被釋放,因此我們一般用完成時間當前系統時間作比較)。DelayQueue的構造方法如下

DelayQueue()
DelayQueue(Collection<? extends E> c)
複製程式碼

這次我們將延遲時間當成是任務開發時間,設定開發時間越短的任務優先順序越高

public class ExecutorTest {
    //省略部分程式碼...
    private static int taskSize = 5;//任務數
    private static int corePoolSize = 0;//核心執行緒的數量
    private static int maximumPoolSize = 5;//執行緒數的最大值

    public static class DelayTask implements Runnable,Delayed{
        private long finishTime;
        private long delay;
        
        public DelayTask(long delay){
            this. delay= delay;
            finishTime = (delay + System.currentTimeMillis());//計算出完成時間
        }
        @Override
        public void run() {
            if (taskSize > 0) {
                try{
                    System.out.println(getTime() + getName(Thread.currentThread().getName())
                            + " 完成一個開發任務,編號為t" + (taskSize--) + ", 用時:" + delay/1000
                    );
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
        @Override
        public long getDelay(@NonNull TimeUnit unit) {
            //將完成時間和當前時間作比較,<=0 時說明元素到期需被釋放
            return (finishTime - System.currentTimeMillis());
        }
        @Override
        public int compareTo(@NonNull Delayed o) {
            DelayTask temp = (DelayTask) o;
            return temp.delay < this.delay?1:-1;//延遲時間越短優先順序越高
        }
    }

    public static void main(String args[]){
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(
                        corePoolSize,
                        maximumPoolSize,
                        1,
                        TimeUnit.SECONDS,
                        new DelayQueue()
                );
        Random random = new Random();
        DelayTask task;
        int size = taskSize;
        for (int i = 0; i < size; i++) {
            long d = 1000 + random.nextInt(10000);
            task = new DelayTask(d);
            executor.execute(task);
            System.out.println("接到任務 " + i + ",預計完成時間為:" + d/1000);
        }
        executor.shutdown();
    }
}
複製程式碼

執行結果如下

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


SynchronousQueue

基於同步的阻塞佇列,這是一個非常特殊的佇列,因為它內部並沒有資料快取空間元素只有在試圖取走的時候才有可能存在。也就是說,如果在插入元素時後續沒有執行取出的操作,那麼插入的行為就會被阻塞,如果SynchronousQueue是線上程池中使用的,那麼這種場景下就會丟擲RejectedExecutionException異常。可能這麼解釋有點繞,下面我們會通過講解示例輔助大家理解,先來看構造方法

SynchronousQueue()
SynchronousQueue(boolean fair)
複製程式碼

同樣的,引數和之前一樣,就不解釋了,我們來看示例:

採用了SynchronousQueue的策略後,任務佇列不能儲存任務了。這意味著如果接到新任務時發現沒人有空來開發(程式設計師手上都有任務,公司招人名額也滿了),那這個新任務就泡湯了(丟擲異常

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

例如我們將核心程式設計師的數量(corePoolSize)設為3,程式設計師總數(maximumPoolSize)設為9,而任務數(taskSize)設為10

public class ExecutorTest {
    //省略部分程式碼...
    private static int taskSize = 10;//任務數
    private static int corePoolSize = 3;//核心執行緒的數量
    private static int maximumPoolSize = 9;//執行緒數的最大值

    public static void main(String args[]){
        ThreadPoolExecutor executor =
                new ThreadPoolExecutor(
                        corePoolSize,
                        maximumPoolSize,
                        1,
                        TimeUnit.SECONDS,
                        new SynchronousQueue<Runnable>()
                );
        TestTask task;
        int size = taskSize;
        for (int i = 0; i < size; i++) {
            task = new TestTask();
            executor.execute(task);
            System.out.println("接到任務 " + i);
        }
        executor.shutdown();
    }
}
複製程式碼

在招滿人的情況下,公司最多就9個程式設計師,當接到第10個任務時,發現沒人可用了,就會丟擲異常。當然,之前成功接收的任務不會受到影響

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

因此根據SynchronousQueue的特性,在使用SynchronousQueue時通常會將maximumPoolSize設為“無邊界”,即Integer.MAX_VALUE(在系統為我們預設的執行緒池中,CachedThreadPool就是這麼設定的,具體的我們後面再細說)


系統預設的執行緒池

前面講了這麼多,其實都是教大家如何自定義一個執行緒池。系統為了方便我們進行開發,早已封裝好了各種執行緒池供我們使用。我們可以用Executors.newXXX的方式去例項化我們需要的執行緒池。可供選擇的執行緒池種類很多:

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

我們挑其中常用的4種講講就行(其實各種執行緒池的區別只是構建執行緒池時傳入的引數不同而已,經過之前我們對任務執行策略和各種任務佇列的講解後,理解不同種類的執行緒池就變得非常簡單了。這也正是博主要花費那麼長的篇幅給大家舉例子的原因,希望大家都能看得懂吧~)

CachedThreadPool

我們直接看系統是如何封裝的

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

核心執行緒數0執行緒總數設為Integer.MAX_VALUE執行緒的閒置時長60s任務佇列SynchronousQueue同步佇列),結合我們之前說的,可以總結出CachedThreadPool的特點如下:

  • CachedThreadPool只有非核心執行緒,當提交任務後,若當前所有已建立的執行緒都在執行任務(或執行緒數為0),則新建立一條執行緒執行新任務
  • 閒置的執行緒超過60s後會被回收
  • 所有提交的任務都會被立即執行(因為任務佇列為SynchronousQueue
  • CachedThreadPool在執行大量短生命週期的非同步任務時,可以顯著提高程式效能

使用示例如下,我們設定任務數為10

ExecutorService service = Executors.newCachedThreadPool();
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
	task = new TestTask();
	service.execute(task);
}
service.shutdown();
複製程式碼

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


FixedThreadPool

原始碼如下

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

核心執行緒數由我們自定義最大執行緒數核心執行緒數相等,執行緒閒置時間0任務佇列LinkedBlockingQueue,所以FixedThreadPool的特點如下:

  • 執行緒數固定,執行緒池中只有核心執行緒,且核心執行緒沒有超時限制
  • 任務佇列容量沒有大小限制
  • FixedThreadPool適用於需要快速響應的場景

使用示例如下,我們設定任務數為10,核心執行緒數為5

ExecutorService service = Executors.newFixedThreadPool(corePoolSize);
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
	task = new TestTask();
	service.execute(task);
}
service.shutdown();
複製程式碼

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


SingleThreadExecutor

原始碼如下

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

核心執行緒數1最大執行緒數也是1執行緒閒置時間0任務佇列LinkedBlockingQueue,且SingleThreadExecutorFinalizableDelegatedExecutorService類的例項,所以SingleThreadExecutor的特點如下:

  • 只有一個核心執行緒,所有任務都在同一執行緒中按順序完成
  • 任務佇列容量沒有大小限制
  • 如果單個執行緒在執行過程中因為某些錯誤而中止,會建立新的執行緒替代它執行後續的任務(區別於 newFixedThreadPool(1) ,如果執行緒遇到錯誤中止,newFixedThreadPool(1) 是無法建立替代執行緒的)
  • 使用SingleThreadExecutor我們就不需要處理執行緒同步的問題了

使用示例如下,我們設定任務數為10

ExecutorService service = Executors.newSingleThreadExecutor();
TestTask task;
int size = taskSize;
for (int i = 0; i < size; i++) {
	task = new TestTask();
	service.execute(task);
}
service.shutdown();
複製程式碼

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解


ScheduledThreadPool

原始碼如下

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

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(
        corePoolSize, 
        Integer.MAX_VALUE,
        DEFAULT_KEEPALIVE_MILLIS, //DEFAULT_KEEPALIVE_MILLIS = 10L
        MILLISECONDS,
        new DelayedWorkQueue()
    );
}
複製程式碼

核心執行緒數由我們自定義最大執行緒數Integer.MAX_VALUE執行緒閒置時長10毫秒任務佇列採用了DelayedWorkQueue(和DelayQueue非常像)。ScheduledThreadPool的特點如下:

  • 核心執行緒數固定,非核心執行緒數無限制
  • 非核心執行緒閒置時會被立即回收
  • 可以執行定時任務具有固定週期的任務

使用示例如下,我們呼叫ScheduledExecutorService.schedule方法提交延遲啟動的任務,延遲時間為3秒

ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3);
Runnable runnable = new Runnable(){
	@Override
	public void run() {
		System.out.println("開始執行任務,時間:" + getTime());
	}
};
scheduledExecutorService.schedule(runnable,3,TimeUnit.SECONDS);
System.out.println("提交任務,時間:" + getTime());
複製程式碼

大話Android多執行緒(五) 執行緒池ThreadPoolExecutor詳解

此外還有scheduleAtFixedRatescheduleWithFixedDelay等提交任務的方法,就不一一舉例了


一些額外的補充

1、我們除了用execute方法提交任務以外,還可以使用submit方法。submit方法提交的任務需實現Callable介面(有關Callable的知識可以看下我上一篇部落格:大話Android多執行緒(四) Callable、Future和FutureTask),因此其具有返回值

2、執行緒池有兩種手動關閉的方法:

  • shutDown():關閉執行緒池後不影響已經提交的任務
  • shutDownNow():關閉執行緒池後會嘗試去終止正在執行任務的執行緒

3、如何合理地估算執行緒池大小?

emmmm...基本就這些內容了,博主已經儘可能地覆蓋執行緒池的所有知識了(除了原始碼解析,以後有機會會出一個單章分析下原始碼),若有什麼遺漏或者建議的歡迎留言評論。如果覺得博主寫得還不錯麻煩點個贊,你們的支援是我最大的動力~

相關文章