Java執行緒池的使用和原理

weixin_34321977發表於2018-10-18

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

  • 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
  • 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
  • 提高執行緒的可管理性。

執行緒是稀缺資源,如果無限制地建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控。

執行緒池的使用

建立執行緒池

可以通過ThreadPoolExecutor來建立一個執行緒池:

    public ThreadPoolExecutor(int corePoolSize,//核心執行緒數量
                              int maximumPoolSize,//最大執行緒數
                              long keepAliveTime,//超時時間,超出核心執行緒數量以外的執行緒空餘存活時間
                              TimeUnit unit,//存活時間單位
                              BlockingQueue<Runnable> workQueue,//儲存執行任務的佇列
                              ThreadFactory threadFactory,//建立新執行緒使用的工廠
                              RejectedExecutionHandler handler)//當任務無法執行的時候的處理方式

workQueue引數可以選擇以下幾個阻塞佇列:
ArrayBlockingQueue:是一個基於陣列結構的有界阻塞佇列,此佇列按FIFO(先進先出)原則對元素進行排序。
LinkedBlockingQueue:一個基於連結串列結構的阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。
SynchronousQueue:一個不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,提交的任務不會被真實的儲存,而總是將新任務提交給執行緒執行,如果沒有空閒的執行緒,則嘗試建立新的執行緒,如果執行緒的數量已經達到了最大值,則執行拒絕策略,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列。
PriorityBlockingQueue:一個具有優先順序的無限阻塞佇列。

handler引數可以以下幾個拒絕策略:
AbortPolicy:直接丟擲異常。
CallerRunsPolicy:使用呼叫者所線上程來執行任務。
DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。
DiscardPolicy:不處理,丟棄掉。
當然,也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化儲存不能處理的任務。

向執行緒池提交任務

可以使用兩個方法向執行緒池提交任務,分別為execute()和submit()方法。
execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功。execute()方法輸入的任務是一個Runnable類的例項。

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

submit()方法用於提交需要返回值的任務,引數可以是是Runnable或Callable。執行緒池會返回一個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狀態,然後中斷所有沒有正在執行任務的執行緒。

只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時呼叫isTerminaed方法會返回true。至於應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫shutdown方法來關閉執行緒池,如果任務不一定要執行完,則可以呼叫shutdownNow方法。

執行緒池的監控

如果在系統中大量使用執行緒池,則有必要對執行緒池進行監控,方便在出現問題時,可以根據執行緒池的使用狀況快速定位問題。可以通過執行緒池提供的引數進行監控,在監控執行緒池的時候可以使用以下屬性。
taskCount:執行緒池需要執行的任務數量。
completedTaskCount:執行緒池在執行過程中已完成的任務數量,小於或等於taskCount。
largestPoolSize:執行緒池裡曾經建立過的最大執行緒數量。通過這個資料可以知道執行緒池是否曾經滿過。如該數值等於執行緒池的最大大小,則表示執行緒池曾經滿過。
getPoolSize:執行緒池的執行緒數量。如果執行緒池不銷燬的話,執行緒池裡的執行緒不會自動銷燬,所以這個大小隻增不減。
getActiveCount:獲取活動的執行緒數。
通過擴充套件執行緒池進行監控。可以通過繼承執行緒池來自定義執行緒池,重寫執行緒池的beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和執行緒池關閉前執行一些程式碼來進行監控。例如,監控任務的平均執行時間、最大執行時間和最小執行時間等。

當向執行緒池提交一個任務之後,執行緒池的處理流程如下:


3972450-9aa646f88bac6520.png

3972450-5fa857e198548bb3.png

執行緒池的原理

執行緒池用一個AtomicInteger來儲存 [執行緒數量] 和 [執行緒池狀態] ,一個int數值一共有32位,高3位用於儲存執行狀態,低29位用於儲存執行緒數量。

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); //一個原子操作類
private static final int COUNT_BITS = Integer.SIZE - 3; //32-3
private static final int CAPACITY  = (1 << COUNT_BITS) - 1; //將1的二進位制向右位移29位,再減
1表示最大執行緒容量
//執行狀態儲存在int值的高3位 (所有數值左移29位)
private static final int RUNNING   = -1 << COUNT_BITS;// 接收新任務,並執行佇列中的任務
private static final int SHUTDOWN  =  0 << COUNT_BITS;// 不接收新任務,但是執行佇列中的任務
private static final int STOP    =  1 << COUNT_BITS;// 不接收新任務,不執行佇列中的任務,中
斷正在執行中的任務
private static final int TIDYING   =  2 << COUNT_BITS; //所有的任務都已結束,執行緒數量為0,處
於該狀態的執行緒池即將呼叫terminated()方法
private static final int TERMINATED =  3 << COUNT_BITS;// terminated()方法執行完成
// Packing and unpacking ctl
private static int runStateOf(int c)   { return c & ~CAPACITY; } //獲取執行狀態
private static int workerCountOf(int c) { return c & CAPACITY; } //獲取執行緒數量

execute()方法原始碼:

public void execute(Runnable command) {
  if (command == null)
    throw new NullPointerException();
   int c = ctl.get();
  if (workerCountOf(c) < corePoolSize) {//1.當前池中執行緒比核心數少,新建一個執行緒執行任務
    if (addWorker(command, true))
      return;
    c = ctl.get();
 }
if (isRunning(c) && workQueue.offer(command)) {//2.核心池已滿,但任務佇列未滿,新增到佇列
中
    int recheck = ctl.get();
//任務成功新增到佇列以後,再次檢查是否需要新增新的執行緒,因為已存在的執行緒可能被銷燬了
    if (! isRunning(recheck) && remove(command))
      reject(command);//如果執行緒池處於非執行狀態,並且把當前的任務從任務佇列中移除成功,則拒
絕該任務
    else if (workerCountOf(recheck) == 0)//如果之前的執行緒已被銷燬完,新建一個執行緒
      addWorker(null, false);
 }
  else if (!addWorker(command, false)) //3.核心池已滿,佇列已滿,試著建立一個新執行緒
    reject(command);  //如果建立新執行緒失敗了,說明執行緒池被關閉或者執行緒池完全滿了,拒絕任務
}

相關文章