執行緒池 Executor

eluanshi12發表於2018-12-24

new Thread的弊端

  • 每次new Thread 新建物件,效能差
  • 執行緒缺乏統一管理,可能無限制的新建執行緒,相互競爭,可能佔用過多的系統資源導致當機或者OOM(out of memory記憶體溢位)。(這種問題的原因不是因為單純的new一個Thread,而是可能因為程式的bug或者設計上的缺陷導致不斷new Thread造成的)
  • 缺少更多功能,如更多執行、定期執行、執行緒中斷。

執行緒池的好處

  1. 降低資源消耗。重複利用已建立的執行緒,減少執行緒建立、消亡的開銷。
  2. 提高響應速度。當任務到達時,可以不需要等到執行緒建立就能立即執行。
  3. 提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。(併發數控制,定時/定期執行等)

執行緒池類圖

執行緒池類圖
常用最下邊的Executors,用它來建立執行緒池使用執行緒。
Executor框架,根據一組執行策略的呼叫排程執行和控制非同步任務,目的是提供一種將任務提交與任務執行分離開的機制。

  • Executor:執行新任務的簡單介面
  • ExecutorService:擴充套件了Executor,新增了用來管理執行器生命週期和任務生命週期的方法
  • ScheduleExcutorService:擴充套件了ExecutorService,支援Future和定期執行任務

執行緒池的實現原理

處理流程

corePoolSize、maximumPoolSize、workQueue 三者關係

  • corePoolSize(核心執行緒數、基本執行緒數):當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務(即使有空閒的基本執行緒。當任務數大於corePoolSize時就不再建立)。如果呼叫了執行緒池prestartAllCoreThreads()方法,執行緒池會提前建立並啟動所有基本執行緒。
  • runnableTaskQueue(任務佇列) :workQueue阻塞佇列,儲存等待執行的任務(執行中的執行緒數大於corePoolSize且小於maximumPoolSize時)
  • maximumPoolSize:執行緒最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。(如果使用了無界的任務佇列,能夠建立的最大執行緒數為corePoolSize,這時maximumPoolSize就不會起作用)

吞吐量:SynchronousQueue高於LinkedBlockingQueue高於ArrayBlockingQueue
ThreadPoolExecutor執行示意圖

ThreadPoolExecutor執行execute()的4種情況

1)如果當前執行的執行緒少於corePoolSize,則建立新執行緒來執行任務(需要獲取全域性鎖)。
2)如果執行的執行緒等於或多於corePoolSize,則將任務加入BlockingQueue。
3)如果無法將任務加入BlockingQueue(佇列已滿),則建立新的執行緒來處理任務(需要獲取全域性鎖)。
4)如果建立新執行緒將使當前執行的執行緒超出maximumPoolSize,任務將被拒絕,並呼叫RejectedExecutionHandler.rejectedExecution()方法。

設計思路,是為了在執行execute()方法時,儘可能地避免獲取全域性鎖 (嚴重的可伸縮瓶頸)。在ThreadPoolExecutor完成預熱之後(當前執行的執行緒數大於等於corePoolSize),幾乎所有的execute()方法呼叫都是執行步驟2,而步驟2不需要獲取全域性鎖。

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
         //只有當前執行緒池中執行緒數poolSize <corePoolSize 時,則建立執行緒並執行當前任務 
        //當poolSize >=corePoolSize 或執行緒建立失敗,則將當前任務放到工作佇列中。  
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {   
            if (runState == RUNNING && workQueue.offer(command)) {
                //如果執行緒池不處於執行中或任務無法放入佇列,並且當前執行緒數量小於最大允許的執行緒數量,則建立一個執行緒執行任務。
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
           //丟擲RejectedExecutionException異常
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); // is shutdown or saturated
        }
    }

工作執行緒: 執行緒池建立執行緒時,會將執行緒封裝成工作執行緒Worker,Worker在執行完任務後,還會迴圈獲取工作佇列裡的任務來執行。Worker類的run()方法:

 public void run() {
	    try {
	    	Runnable task = firstTask;
	    	firstTask = null;
	    	while (task != null || (task = getTask()) != null) {
		    	runTask(task);
		    	task = null;
	    	}
	    } finally {
	    	workerDone(this);
    	}
    }

執行緒池的建立

我們可以通過ThreadPoolExecutor來建立一個執行緒池。

new ThreadPoolExecutor(int corePoolSize, int maximumPoolSize,long  keepAliveTime,TimeUnit unit  ,
BlockingQueue<Runnable> workQueue ,ThreadFactory threadFactory, RejectedExecutionHandler handler);

ThreadFactory:執行緒工廠,設定建立執行緒,可以通過執行緒工廠給每個建立出來的執行緒設定名稱。開源框架guava提供的ThreadFactoryBuilder可以快速給執行緒池裡的執行緒設定有意義的名字,程式碼如下。

new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();

RejectedExecutionHandler(飽和策略):當佇列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。

.AbortPolicy:直接丟擲異常。
·CallerRunsPolicy:使用 【呼叫者】所線上程(execute 方法的呼叫執行緒) 來執行任務
(生產者的當前執行緒。沒有開啟新執行緒或交給執行緒池來排程)。
·DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。
·DiscardPolicy:不處理,丟棄掉。

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

keepAliveTime(執行緒活動保持時間):執行緒池的工作執行緒空閒後,保持存活的時間。所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高執行緒的利用率。

TimeUnit:keepAliveTime的時間單位

向執行緒池提交任務

execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功。
通過以下程式碼可知execute()方法輸入的任務是一個Runnable類的例項。

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

submit()方法用於提交需要返回值的任務。執行緒池會返回一個future 型別的物件,通過這個future物件可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前執行緒直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前執行緒一段時間後立即返回,這時候有可能任務沒有執行完。

Future<Object> future = executor.submit(harReturnValuetask);
	try {
		Object s = future.get();//獲取返回值
	} catch (InterruptedException e) {
		// 處理中斷異常
	} catch (ExecutionException e) {
		// 處理無法執行任務異常
	} finally {
		// 關閉執行緒池
		executor.shutdown();
	}

關閉執行緒池

執行緒池生命週期
shutdown或shutdownNow方法關閉執行緒池。

原理:遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。

區別

  • shutdownNow()將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表
  • shutdown()將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒。

其他狀態

  • SHUTDOWN不能處理新的任務,但是能繼續處理阻塞佇列中任務
  • stop:不能接收新的任務,也不處理佇列中的任務
  • tidying:如果所有的任務都已經終止了,這時有效執行緒數為0
  • terminated:最終狀態

合理地配置執行緒池

必須首先分析任務特性,可以從以下幾個角度來分析。

  • 任務的性質:CPU密集型任務、IO密集型任務和混合型任務。
  • 任務的優先順序:高、中和低。
  • 任務的執行時間:長、中和短。
  • 任務的依賴性:是否依賴其他系統資源,如資料庫連線。

性質不同的任務可以用不同規模的執行緒池分開處理。

  • CPU密集型任務應配置儘可能小的執行緒,如配置Ncpu+1個執行緒的執行緒池。
  • 由於IO密集型任務執行緒並不是一直在執行任務,則應配置儘可能多的執行緒,如2*Ncpu。
  • 混合型的任務,如果可以拆分,將其拆分成一個CPU密集型任務和一個IO密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於序列執行的吞吐量。如果這兩個任務執行時間相差太大,則沒必要進行分解。
  • 依賴資料庫連線池的任務,因為執行緒提交SQL後需要等待資料庫返回結果,等待的時間越長,則CPU空閒時間就越長,那麼執行緒數應該設定得越大,這樣才能更好地利用CPU。

可以通過Runtime.getRuntime().availableProcessors()方法獲得當前裝置的CPU個數。
優先順序不同的任務可以使用優先順序佇列PriorityBlockingQueue來處理。
注意:如果一直有優先順序高的任務提交到佇列裡,那麼優先順序低的任務可能永遠不能執行。

建議使用有界佇列
最大maximumPoolSize,能增加系統的穩定性和預警能力,能夠降低資源消耗但是這種方式使得執行緒池對執行緒排程變的更困難。
想讓【執行緒池的吞吐率】【處理任務】達到一個合理的範圍,使【執行緒排程相對簡單】【儘可能降低資源消耗】合理限制【執行緒池】與【佇列容量】

分配技巧

  1. 降低資源消耗【cpu使用率、作業系統資源的消耗、上下文切換的開銷】設定一個【較大的佇列容量】【較小執行緒池容量】降低執行緒池吞吐量
  2. 提交的任務經常發生阻塞,可以調整maximumPoolSize
  3. 佇列容量較小,需要把執行緒池大小設定的大一些,這樣cpu的使用率相對來說會高一些
  4. 如果執行緒池的容量設定的過大,提高任務的數量過多的時候,併發量會增加,需要考慮執行緒之間的排程。這樣反而可能會降低處理任務的吞吐量。

執行緒池的監控

·taskCount:執行緒池需要執行的任務數量。
·completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。
·largestPoolSize:執行緒池裡曾經建立過的最大執行緒數量。通過這個資料可以知道執行緒池是否曾經滿過。如該數值等於執行緒池的最大大小,則表示執行緒池曾經滿過。
·getPoolSize:執行緒池的執行緒數量。如果執行緒池不銷燬的話,執行緒池裡的執行緒不會自動銷燬,所以這個大小隻增不減。
·getActiveCount:獲取活動的執行緒數。

通過擴充套件執行緒池進行監控。可以通過繼承執行緒池來自定義執行緒池,重寫執行緒池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和執行緒池關閉前執行一些程式碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。

protected void beforeExecute(Thread t, Runnable r) {
	這幾個方法線上程池裡是空方法。
 }

相關文章