Java進階之深入理解線程池

1 線程池概念

1.1 概念

線程池,就是一個線程的池子,裏面有若干線程,它們的目的就是執行提交給線程池的任務,執行完一個任務後不會退出,而是繼續等待或執行新任務。

1.2 爲什麼要用線程池?

(1)降低資源消耗: 通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
(2)提高響應速度: 當任務到達時,任務可以不需要的等到線程創建就能立即執行。
(3)提高線程的可管理性: 線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。

2 線程池的實現原理

2.1 ThreadPoolExecutor執行execute()方法的流程圖

在這裏插入圖片描述

2.2 ThreadPoolExecutor執行execute()方法的示意圖

2.2.1 示意圖

在這裏插入圖片描述

2.2.2 示意圖步驟分析

(1)如果當前運行線程少於corePoolSize,就創建新線程執行任務(執行這一步需要獲取全局鎖);
(2)如果運行線程等於或多於corePoolSize,就將任務加入BlockingQueue;
(3)如果BlockingQueue已滿,創建新線程處理任務;
(4)如果超過最大線程數maximumPoolSize,任務將被拒絕,調用RejectedExecutionHandler.rejectedExecution()方法;

2.2.3 總體設計思路

ThreadPoolExecutor上述步驟總體設計思路:執行execute()方法時,儘可能避免獲取全局鎖(一個嚴重的可伸縮瓶頸)。ThreadPoolExecutor完成預熱之後(當前運行線程數大於等於corePoolSize),幾乎所有的execute方法都是執行步驟2,步驟2不需要獲取全局鎖。

2.2.4 源碼分析

(1)execute()源碼分析
在這裏插入圖片描述
(2)工作線程:線程池創建線程時,會將線程封裝成工作線程worker,worker執行完任務後,還會循環獲取工作隊列的任務來執行
在這裏插入圖片描述
在這裏插入圖片描述
①execute方法創建一個線程時,會讓這個線程執行當前任務;
②這個線程執行完上圖1的任務後,會反覆從BlockingQueue獲取任務執行。

3 線程池的使用

3.1 線程池的創建

ThreadPoolExecutor有多個構造方法,都需要一些參數,主要構造方法有:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
	this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);
}                    

線程池需要的參數分析如下:

(1)corePoolSize:表示線程池中的核心線程個數,但並不是一開始就創建這麼多線程,剛創建一個線程池後,不會預先創建核心線程,只有當有任務時纔會創建;而且核心線程不會因爲空閒而被終止,keepAliveTime參數不適用於它。如果調用了線程池的prestartAllCoreThreads()方法,線程池會提前創建並啓動所有基本線程。

(2)maximumPoolSize:表示線程池中的最大線程數量,如果隊列滿了,並且已創建的線程數小於最大線程數,則線程池會再創建新的線程執行任務。使用了無界隊列,這個參數就沒有什麼效果。

(3)keepAliveTime:表示當線程池中的線程個數大於corePoolSize時,額外空閒線程的存活時間。也就是說,一個非核心線程,在空閒等待新任務時,會有一個最長等待時間,即keepAliveTime,如果到了時間還是沒有新任務,就會被終止。如果該值爲0,表示所有線程都不會超時終止

(4)runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列,它們都可以用作線程池的隊列,比如:
①直接提交。工作隊列的默認選項是synchronousQueue,它將任務直接提交給線程而不保持它們。在此,如果不存在可用於立即運行任務的線程,則試圖把任務加入隊列將失敗,因此會構造一個新的線程。此策略可以避免在處理可能具有內部依賴性的請求集時出現鎖。直接提交通常要求無界maximumPoolSizes以避免拒絕新提交的任務。當命令以超過隊列所能處理的平均數連續到達時,此策略允許無界線程具有增加的可能性。

②無界隊列。使用無界隊列(例如:不具有預定義容量的LinkedBlockingQueue)將導致在所有corePoolSize線程都忙時新任務在隊列中等待。這樣,創建的線程就不會超過corePoolSize(因此,maximumPoolSize的值也就無效了)。

③有界隊列。當使用有限的maximumPoolSizes時,有界隊列(例如:ArrayBlockingQueue)有助於防止資源耗盡,但是可能較難調整和控制。隊列大小和最大池大小可能需要相互折衷:使用大型隊列和小型池可以最大限度的降低CPU使用率、操作系統資源和上下文切換開銷,但是可能導致人工降低吞吐量。如果任務頻繁阻塞,則系統可能爲超過您許可的更多線程安排時間,使用小型隊列通常要求較大的池大小,CPU使用率較高,但是可能遇到不可接受的調度開銷,這樣可會降低吞吐量。

④PriorityBlockingQueue:一個具有優先級的無界阻塞優先級隊列。

(5)ThreadFactory:用於設置創建線程的工廠,可以通過線程工廠給每個創建出來的線程設置更有意義的名字。

(6)RejectedExecutionHandler(任務拒絕策略):當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認是AbortPolicy,表示無法處理新任務時拋出異常。ThreadPoolExecutor實現了四種處理方式:
①AbortPolicy:這就是默認的方式,拋出異常;
②DiscardPolicy:不拋異常,也不執行;
③DiscardOldestPolicy:丟棄隊列中最近的一個任務,並執行當前任務;
④CallerRunsPolicy:在任務提交者線程中執行任務,不提交給線程池中的線程執行。

(7)keepAliveTime(線程活動保持時間):線程池的非核心線程中的空閒線程的存活時間。所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高線程的利用率。

(8)TimeUnit(線程活動保持時間的單位):可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)

3.2 向線程池提交任務

(1)execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被線程池執行成功

threadsPool.execute(new Runnable() {
	@Override
	public void run() {
		// TODO Auto-generated method stub
	}
});

(2)submit()方法用於提交需要返回值的任務。一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前線程直到任務完成
在這裏插入圖片描述

3.3 關閉線程池

3.4 合理配置線程池

參考阿里Android手冊的強制要求。線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。Executors 返回的線程池對象的弊端如下:

 - FixedThreadPool和SingleThreadPool : 允 許 的 請 求 隊 列 長 度 爲Integer.MAX_VALUE,可能會堆積大量的請求,從而導致OOM;
 - CachedThreadPool和ScheduledThreadPool : 允 許 的 創 建 線 程 數 量 爲Integer.MAX_VALUE,可能會創建大量的線程,從而導致OOM。
// 正例
// 返回可用處理器的Java虛擬機的數量
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
// 使用有界隊列,可以配置大一些,例如幾千
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>(3000);

ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES*2, 				 		
														KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT,
														taskQueue, new BackgroundThreadFactory(), 
														new DefaultRejectedExecutionHandler());
//反例
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

3.5 線程池的監控

4 線程池的類型及區別

類Executors提供了一些靜態工廠方法,可以方便的創建一些預配置的線程池,主要方法有:

  • newSingleThreadExecutor創建一個單核心線程數量(核心==最大)的線程池,它只會用唯一的線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。因此適用於需要確保所有任務被順序執行的場合。
public static ExecutorService newSingleThreadExecutor() {
    return new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>());
}

只使用一個線程,使用無界隊列LinkedBlockingQueue,線程創建後不會超時終止,該線程順序執行所有任務。

  • newFixedThreadPool創建一個線程數量固定(核心==最大)的線程池,且不會被回收,可控制線程最大併發數,超出的線程會在隊列中等待。能夠更快的響應外界請求,比較適合在系統負載高下,通過隊列對新任務排隊,保證有足夠的資源處理實際的任務。
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

使用固定數目的n個線程,使用無界隊列LinkedBlockingQueue,線程創建後不會超時終止。和newSingleThreadExecutor一樣,由於是無界隊列,如果排隊任務過多,可能會消耗非常大的內存。

  • newCachedThreadPool創建一個核心線程爲0的可緩存線程池,非核心線程數量相當於無限大,任何任務都會被立即執行。比較適合在系統負載不太高下,執行大量的執行時間比較短的任務。
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

它的corePoolSize爲0,maximumPoolSize爲Integer.MAX_VALUE,keepAliveTime是60秒,隊列爲SynchronousQueue。含義是,當新任務到來時,如果正好有空閒線程在等待任務,則其中一個空閒線程接受該任務,否則就總是創建一個新線程,創建的總線程個數不受限制。對任一空閒線程,如果60秒內沒有新任務,就終止。

  • newScheduledThreadPool創建一個核心線程數量固定的線程池,非核心線程數量不定的線程池,支持定時及週期性任務執行。適合執行定時任務或者具有周期性的重複任務。
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}

ScheduledThreadPool的核心線程數量是固定的,由傳入的corePoolSize參數決定,非核心線程數量可以無限大。非核心線程閒置回收的超時時間爲10秒( DEFAULT_KEEPALIVE_MILLIS的值爲10L)。

6 AsnyncTask的線程池申請

    //獲取當前的cpu核心數  
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();  
    //線程池核心容量  
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;  
    //線程池最大容量  
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;  
    //過剩的空閒線程的存活時間  
    private static final int KEEP_ALIVE = 1;  
    //ThreadFactory 線程工廠,通過工廠方法newThread來獲取新線程  
    private static final ThreadFactory sThreadFactory = new ThreadFactory() {  
        //原子整數,可以在超高併發下正常工作  
        private final AtomicInteger mCount = new AtomicInteger(1);  

        public Thread newThread(Runnable r) {  
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());  
        }  
    };  
    //靜態阻塞式隊列,用來存放待執行的任務,初始容量:128個  
    private static final BlockingQueue<Runnable> sPoolWorkQueue =  
            new LinkedBlockingQueue<Runnable>(128);  

    /**
     * 靜態併發線程池,可以用來並行執行任務,儘管從3.0開始,AsyncTask默認是串行執行任務 
     * 但是我們仍然能構造出並行的AsyncTask 
     * 
     * CORE_POOL_SIZE 核心線程數
     * MAXIMUM_POOL_SIZE 最大線程數量
     * KEEP_ALIVE 1s閒置回收
     * TimeUnit.SECONDS 時間單位
     * sPoolWorkQueue 異步任務隊列
     * sThreadFactory 線程工廠
     */
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
            TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory,
            new ThreadPoolExecutor.DiscardOldestPolicy());

7 總結

ThreadPoolExecutor實現了生產者/消費者模式,工作者線程就是消費者,任務提交者就是生產者,線程池自己維護任務隊列。當我們碰到類似生產者/消費者問題時,應該優先考慮直接使用線程池,而非重新發明輪子,自己管理和維護消費者線程及任務隊列。

8 參考鏈接

Java併發編程的藝術:P200-227

計算機程序的思維邏輯 (78) - 線程池

必須要理清的Java線程池

Java線程池ThreadPoolExecutor實現原理

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