1. 程式人生 > >執行緒池及其執行原理

執行緒池及其執行原理

前言

  • 首先從結構說起
  • 然後執行緒池的引數
  • 最後在結合程式碼簡單分析

new Thread 弊端

        第一:每次new Thread 新建物件,效能差
        第二:執行緒缺乏統一管理,可能無限制的新建執行緒,相互競爭,有可能佔用過多系統資源導致宕機或OOM
        第三:缺少更多的功能,如更多執行、定期執行、執行緒中斷。


什麼是執行緒池

Java中的執行緒池是運用場景最多的併發框架,幾乎所有需要非同步或併發執行任務的程式,都可以使用執行緒池。在開發過程中,合理地使用執行緒池能夠帶來3個好處。
           第一:降低資源消耗

。重用存在的執行緒,減少物件建立、消亡的開銷,效能佳。
           第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
           第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。但是,要做到合理利用、


執行緒池例項的幾種狀態

           running狀態:能接受新提交的任務,並且能處理阻塞佇列中的任務。
           shutdown狀態:當一個執行緒池例項處於關閉狀態的時候,不能在接收新提交的任務,但是可以繼續處理阻塞佇列中已經儲存的任務。線上程池處理running狀態時,它呼叫shutdown方法,會使執行緒池進入到該狀態。
           stop狀態:不能接收新的任務,也不處理佇列中的任務。它會中斷正在處理中的執行緒,線上程池處於running或shutdown狀態時,如果呼叫shutdownNow的時候會使執行緒池進入到該狀態
           tidying狀態:如果所用的任務都終止了,有效執行緒數為0,執行緒池會進入到該狀態。之後呼叫terminated()方法會進入terminated狀態。
           terminated狀態


執行緒池的體系結構

根目錄是Executor在JUC包下(java.util.concurrent),結構如下(這裡說的是在JUC包下的子類):

|- Executor :負責執行緒的使用與呼叫的根介面
    |--** ExecutorService    子介面:執行緒池的主要介面
        |-- AbstractExecutorService 提供 ExecutorService 執行方法的預設實現
              |-- DelegatedExecutorService 
              |-- ForkJoinPool
              |-- ThreadPoolExecutor    執行緒池的實現類
        |-- ScheduledExecutorService    子介面:負責執行緒的排程
              |-- ScheduledThreadPoolExecutor:繼承了 ThreadPoolExecutor,實現了ScheduledExecutorService

實際上最根本用的是ExecutorService,又因為ExecutorService是介面,介面不能建立物件,所以根本用的就是建立ThreadPoolExecutor的例項或ScheduledThreadPoolExecutor的例項。

但是基本上都是使用Executors工具類。最常見的有如下四個。但是這四個類都有可能OOM,一般情況下都需要根據自己需求,自定義執行緒池。

1、newFixedThreadPool()   :  建立固定大小的執行緒池,返回型別是ExecutorService。
2、newCachedThreadPool() :  快取執行緒池,執行緒池的數量不固定,可以根據需求自動的更改數量。返回型別是ExecutorService。
3、newSingleThreadExecutor() :   建立單個執行緒池,執行緒池中只有一個執行緒。返回型別是ExecutorService。
4、newScheduledThreadPool()  :  建立固定大小的執行緒池,可以延遲或定時的執行任務。返回型別ScheduledExecutorService。

ThreadPoolExecutor常用的方法

  • execute():提交任務,交給執行緒池執行。
  • submit():提交任務,能夠返回執行結果 execute + Future
  • shutdown():關閉執行緒池,等待任務都執行完
  • shutdownNow():關閉執行緒池,不等待任務執行完
  • getTaskCount():執行緒池已執行和未執行的任務總數
  • getCompletedTaskCount():已完成的任務數量
  • getPoolSize():執行緒池當前的執行緒數量
  • getActiveCount();當前執行緒池中正在執行任務的執行緒數量

執行緒池工具類的引數介紹

在介紹執行緒池原理的時候首先看一下這四個工具類原始碼。 

           其實這四個工具類都是例項化ThreadPoolExecutor這個類。進入ThreadPoolExecutor裡檢視這個建構函式。ThreadPoolExecutor建構函式有4種。這裡介紹引數最多的。

public ThreadPoolExecutor(int corePoolSize,   //核心執行緒數
                          int maximumPoolSize,//最大執行緒數
                          long keepAliveTime,//表示執行緒沒有任務執行時最多保持多久時間會終止。
                          TimeUnit unit,     //引數keepAliveTime的時間單位,有7種取值
                          BlockingQueue<Runnable> workQueue, //阻塞佇列,儲存等待執行的任務,很重要,會對執行緒池執行過程產生重大影響
                          ThreadFactory threadFactory,  //執行緒工廠,用來建立執行緒
                          RejectedExecutionHandler handler //當拒絕處理任務時的策略
)
拒絕策略有四種
    1、AbortPolicy 預設 處理程式遭到拒絕將丟擲執行時 RejectedExecutionException 
    2、CallerRunsPolicy  用呼叫者所在的執行緒呼叫任務
    3、DiscardOldestPolicy 如果執行程式沒有關閉,阻塞佇列頭部的任務將被刪除,然後重試執行程式(如果再次失敗,則重複此過程)
    4、DiscardPolicy  不能執行的任務將被刪除,只不過不丟擲異常。

    如果執行的執行緒數少於corePoolSize的時候,直接建立新執行緒處理任務。
    如果執行的執行緒數大於等於corePoolSize,小於maximumPoolSize的時候,只有當workQueue滿的時候才建立新的執行緒去處理任務。
    如果設定的corePoolSize和maximumPoolSize相同的話,那麼建立執行緒池大小是固定的。這時候如果有請求,workQueue還沒滿的時候,就把請求放入workQueue中,等待有空閒的執行緒從這裡面提取。

為什麼執行緒池要用阻塞佇列而不用非阻塞佇列?
           因為執行緒執行需要時間,當佇列滿的情況下,遇到新的任務新增不進去,會出現丟任務的情況。用阻塞佇列的話,可以阻
塞新增,等執行緒執行完,有空餘執行緒,會執行阻塞佇列裡的任務,這樣新的任務可以新增進去。

接下來具體介紹執行原理,以及核心執行緒數與最大執行緒數的關係。


執行緒池原理剖析

提交一個任務到執行緒池中,執行緒池的處理流程有三步:

1、判斷執行緒池裡的核心執行緒是否都在執行任務,如果不是(核心執行緒空閒或者還有核心執行緒沒有被建立)則建立一個新的工作執行緒來執行任務。如果核心執行緒都在執行任務,則進入下個流程。

2、執行緒池判斷阻塞佇列是否已滿,如果阻塞佇列沒有滿,則將新提交的任務儲存在這個阻塞佇列裡。如果阻塞佇列滿了,則進入下個流程。

3、判斷執行緒池裡的執行緒是否都處於工作狀態,如果沒有,則建立一個新的工作執行緒來執行任務(也是放線上程池裡)。如果已經滿了,則交給飽和策略來處理這個任務。

 


自定義執行緒池

 如果上圖不理解的話,可以結合程式碼在想著可能會更好理解。根據四個工具類可以得出想要自定義執行緒池就例項化ThreadPoolExecutor即可,就是根據需要例項化ThreadPoolExecutor,如下圖,自定義一個核心執行緒數為1,最大執行緒數為2,阻塞佇列為ArrayBlockingQueue。

public class Test {
	public static void main(String[] args) {
            // 核心執行緒數是1,最大執行緒數是2,阻塞佇列是採用ArrayBlockingQueue(有邊界的阻塞佇列)
	    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 2, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(3));
            //當執行第一個任務時,直接從執行緒池中取出執行緒執行
	    threadPoolExecutor .execute(new TaskThread("任務1"));
            //當執行第二個的時候,因為大於核心執行緒數,所以放在阻塞佇列中
            threadPoolExecutor .execute(new TaskThread("任務2"));
            //當執行第三個的時候,因為大於核心執行緒數,所以放在阻塞佇列中
            threadPoolExecutor .execute(new TaskThread("任務3"));
            //當執行第四個的時候,因為大於核心執行緒數,所以放在阻塞佇列中
            threadPoolExecutor .execute(new TaskThread("任務4"));
            //當執行第五個的時候,因為大於核心執行緒數,且阻塞佇列已經被佔滿,這個時候最大執行緒數還有空閒執行緒,所以新建一個執行緒執行該任務
            threadPoolExecutor .execute(new TaskThread("任務5"));
            //當放入第六個執行緒的時候,因為2個最大執行緒數和3個阻塞佇列全被佔用,所以會報錯
            threadPoolExecutor .execute(new TaskThread("任務6"));
            //關閉執行緒池
	    threadPoolExecutor .shutdown();
	}
}
class TaskThread implements Runnable {
	private String taskName;
	public TaskThred(String taskName) {
		this.taskName = taskName;
	}
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName()+taskName);
	}
}

 


執行緒池的合理配置

1、CPU密集型:花費了絕大多數時間在計算上

CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速執行。 
           CPU密集任務只有在真正的多核CPU上才可能得到加速(通過多執行緒),而在單核CPU上,無論你開幾個模擬的多執行緒,該任務都不可能得到加速,因為CPU總的運算能力就那些。

CPU密集型任務,就需要儘量壓榨CPU,參考值可以設為CPU+1

2、I/O密集型:  花費了大多是時間在等待I/O上。

IO密集型,即該任務需要大量的IO,即大量的阻塞。在單執行緒上執行IO密集型的任務會導致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務中使用多執行緒可以大大的加速程式執行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。

IO密集型任務,參考值可以設定為2*CPU。