題解 P1891 【瘋狂 LCM】
1.原理
1.1為什麼使用執行緒池?
執行緒池體現了一種池化的思想,有些類似於mysql的連線池。我們在使用多執行緒的時候,阿里規約會建議我們使用執行緒池,而不是直接new Thread。我認為執行緒池主要體現在以下幾個優點:
- 便於管理執行緒,比如,可以自定義控制執行緒的建立數量。因為可以實現執行緒的統一分配,呼叫及監控。
- 降低系統的資源消耗,通過重用已存在的執行緒,降低執行緒建立和銷燬造成的消耗。
- 提高響應的速度,這裡主要體現在任務到達時,不用再等待執行緒建立便可以直接呼叫。
1.2 執行緒池原理
執行緒池的主要執行流程如下:
ThreadPoolExecutor執行示意圖如下:
-
corePool核心執行緒池是否已滿,如果未滿,則建立新執行緒來執行任務。
-
corePool已滿,則將任務存至BlockingQueue
工作佇列中。 -
若建立後的執行緒數量將大於maximumPoolSize,則呼叫RejectedExecutionHandler的策略來處理此類未執行的任務,若建立執行緒後的資料仍然小於等於maximumPoolSize,則直接建立執行緒來執行任務。
以上三步中,只有第二步不會要求獲取全域性鎖。在設計上已經盡力避免進行獲取全域性鎖的這個消耗操作,所以一般在第二步的時候就已經可以處理完成任務了。
為什麼設計的時候會區分核心執行緒池和最大執行緒池呢?
個人理解是為了追求效能與資源消耗的極致吧,假如核心執行緒池足以滿足任務執行的需求,那就不必再建立核心執行緒池之外的執行緒,同時,真的出現執行緒不夠用的時候,同樣也會給它一個機會來建立一個執行緒
執行。儘量不出現無法執行任務的情況。只是個人猜想,望指正。
2.使用
2.1 建立
構造方法
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { if (corePoolSize < 0 || maximumPoolSize <= 0 || maximumPoolSize < corePoolSize || keepAliveTime < 0) throw new IllegalArgumentException(); if (workQueue == null || threadFactory == null || handler == null) throw new NullPointerException(); this.corePoolSize = corePoolSize; this.maximumPoolSize = maximumPoolSize; this.workQueue = workQueue; this.keepAliveTime = unit.toNanos(keepAliveTime); this.threadFactory = threadFactory; this.handler = handler; }
-
corePoolSize:核心執行緒池大小。當提交一個任務到執行緒池時,執行緒池會建立一個執行緒來執行任務,即使存在空閒的核心執行緒能夠執行任務,其仍會建立執行緒,等到需要執行的任務數大於執行緒池基本大小時就不再建立。
-
maximumPoolSize:最大執行緒數大小。執行緒池允許建立的最大執行緒數。如果佇列滿了,並且已建立的執行緒數小於最大執行緒數,則執行緒池會再建立新的執行緒執行任務。值得注意的是,如果使用了無界的任務佇列這個引數就沒什麼效果。
-
keepAliveTime:執行緒存活時間。這裡是指工作執行緒在執行任務進入空閒狀態的時候。當有需要任務需要頻繁執行的時候,可以調節此引數來使執行緒存活時間增加,避免頻繁建立新執行緒造成資源的浪費。
-
TimeUnit:執行緒存活時間單位。
-
ThreadFactory:用於設定建立執行緒的工廠,可以通過執行緒工廠給每個創建出來的執行緒設定更有意義的名字 。
-
workQueue:任務工作佇列。用於儲存等待執行的任務的阻塞佇列。有以下幾種:
-
ArrayBlockingQueue:基於陣列結構的有界阻塞佇列,FIFO的原則進行元素的排序。
-
LinkedBlockingQueue: 基於連結串列結構的阻塞佇列,此佇列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個佇列。
-
SynchronousQueue:不儲存元素的阻塞佇列。每個插入操作必須等到另一個執行緒呼叫移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個佇列,個人理解此佇列為了保證每個任務儘可能的被執行處理。
-
PrirorityBlockingQueue:具有優先順序的無限阻塞佇列。
-
-
RejectedExecutionHandler:飽和策略。當工作佇列和執行緒池無法接收或執行任務時,則採用一種策略來處理提交的任務。
-
AbortPolicy:直接丟擲異常
-
CallerRunsPolicy:只用呼叫者所線上程來執行任務。
-
DiscardOldestPolicy:丟棄佇列裡最近的一個任務,並執行當前任務。
-
DiscardPolicy:不處理任務,直接丟棄掉。
-
2.2 執行
向執行緒池提交任務主要有兩個方法:
-
execute()方法
提交一個不需要返回值的任務,所以無法判斷任務是否被執行緒池執行。例如:
threadPoolExecutor.execute(new Runnable() { @Override public void run() { //do something } });
-
submit()方法
可以返回任務執行的結果,通過Future物件來包裝執行的結果,Future物件可以獲取任務是否執行成功,任務執行的結果等。
Future future = threadPoolExecutor.submit(()->{ try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName()); });
2.3 關閉
使用shutdown或shutdownNow方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個呼叫執行緒的interrupt方法來中斷
執行緒,因此無法響應中斷的任務可能永遠無法終止。
二者的區別是,shutdownNow首先將執行緒池的狀態設定成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任
務的列表,而shutdown只是將執行緒池的狀態設定成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒,算是我們常說的優雅的
關閉方式。
只要呼叫了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時調
用isTerminaed方法會返回true。至於應該呼叫哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常呼叫shutdown方
法來關閉執行緒池,如果任務不一定要執行完,則可以呼叫shutdownNow方法。
2.4 配置
技術離不開業務需求,所以線上程池的配置上也離不開實際的需求。比如:當需要處理提交頻繁的任務時,我們可以嘗試將執行緒的存活時間調大。再或者,如果為了保持應用的穩定性,我們可以使用有界的阻塞佇列,避免佇列中執行緒任務不斷積壓,導致記憶體崩潰。
參考:《Java併發程式設計的藝術》