Java併發程式設計的藝術(十)——執行緒池(1)
阿新 • • 發佈:2022-05-03
執行緒池的作用
- 減少資源的開銷 減少了每次建立執行緒、銷燬執行緒的開銷。
- 提高響應速度 每次請求到來時,由於執行緒的建立已經完成,故可以直接執行任務,因此提高了響應速度。
- 提高執行緒的可管理性 執行緒是一種稀缺資源,若不加以限制,不僅會佔用大量資源,而且會影響系統的穩定性。 因此,執行緒池可以對執行緒的建立與停止、執行緒數量等等因素加以控制,使得執行緒在一種可控的範圍內執行,不僅能保證系統穩定執行,而且方便效能調優。
執行緒池的實現原理
執行緒池一般由兩種角色構成:多個工作執行緒 和 一個阻塞佇列。
- 工作執行緒 工作執行緒是一組已經處在執行中的執行緒,它們不斷地向阻塞佇列中領取任務執行。
- 阻塞佇列 阻塞佇列用於儲存工作執行緒來不及處理的任務。當工作執行緒都在執行任務時,到來的新任務就只能暫時在阻塞佇列中儲存。
ThreadPoolExecutor的使用
建立執行緒池
通過如下程式碼即可建立一個執行緒池:
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, timeUnit, runnableTaskQueue, handler);
- corePoolSize:基本執行緒數量 它表示你希望執行緒池達到的一個值。執行緒池會盡量把實際執行緒數量保持在這個值上下。
- maximumPoolSize:最大執行緒數量
這是執行緒數量的上界。
如果實際執行緒數量達到這個值:
- 阻塞佇列未滿:任務存入阻塞佇列等待執行
- 阻塞佇列已滿:呼叫飽和策略
- keepAliveTime:空閒執行緒的存活時間 當實際執行緒數量超過corePoolSize時,若執行緒空閒的時間超過該值,就會被停止。 PS:當任務很多,且任務執行時間很短的情況下,可以將該值調大,提高執行緒利用率。
- timeUnit:keepAliveTime的單位
- runnableTaskQueue:任務佇列
這是一個存放任務的阻塞佇列,可以有如下幾種選擇:
- ArrayBlockingQueue 它是一個由陣列實現的阻塞佇列,FIFO。
- LinkedBlockingQueue 它是一個由連結串列實現的阻塞佇列,FIFO。 吞吐量通常要高於ArrayBlockingQueue。 fixedThreadPool使用的阻塞佇列就是它。 它是一個無界佇列。
- SynchronousQueue 它是一個沒有儲存空間的阻塞佇列,任務提交給它之後必須要交給一條工作執行緒處理;如果當前沒有空閒的工作執行緒,則立即建立一條新的工作執行緒。 cachedThreadPool用的阻塞佇列就是它。 它是一個無界佇列。
- PriorityBlockingQueue 它是一個優先權阻塞佇列。
- handler:飽和策略
當實際執行緒數達到maximumPoolSize,並且阻塞佇列已滿時,就會呼叫飽和策略。
JDK1.5由四種飽和策略:
- AbortPolicy 預設。直接拋異常。
- CallerRunsPolicy 只用呼叫者所在的執行緒執行任務。
- DiscardOldestPolicy 丟棄任務佇列中最久的任務。
- DiscardPolicy 丟棄當前任務。
提交任務
可以向ThreadPoolExecutor提交兩種任務:Callable和Runnable。
- Callable 該類任務有返回結果,可以丟擲異常。 通過submit函式提交,返回Future物件。 可通過get獲取執行結果。
- Runnable 該類任務只執行,無法獲取返回結果,並在執行過程中無法拋異常。 通過execute提交。
關閉執行緒池
關閉執行緒池有兩種方式:shutdown和shutdownNow,關閉時,會遍歷所有的執行緒,呼叫它們的interrupt函式中斷執行緒。但這兩種方式對於正在執行的執行緒處理方式不同。
- shutdown() 僅停止阻塞佇列中等待的執行緒,那些正在執行的執行緒就會讓他們執行結束。
- shutdownNow() 不僅會停止阻塞佇列中的執行緒,而且會停止正在執行的執行緒。
ThreadPoolExecutor執行機制
當有請求到來時:
- 若當前實際執行緒數量 少於 corePoolSize,即使有空閒執行緒,也會建立一個新的工作執行緒;
- 若當前實際執行緒數量處於corePoolSize和maximumPoolSize之間,並且阻塞佇列沒滿,則任務將被放入阻塞佇列中等待執行;
- 若當前實際執行緒數量 小於 maximumPoolSize,但阻塞佇列已滿,則直接建立新執行緒處理任務;
- 若當前實際執行緒數量已經達到maximumPoolSize,並且阻塞佇列已滿,則使用飽和策略。
設定合理的執行緒池大小
任務一般可分為:CPU密集型、IO密集型、混合型,對於不同型別的任務需要分配不同大小的執行緒池。
- CPU密集型任務 儘量使用較小的執行緒池,一般為CPU核心數+1。 因為CPU密集型任務使得CPU使用率很高,若開過多的執行緒數,只能增加上下文切換的次數,因此會帶來額外的開銷。
- IO密集型任務 可以使用稍大的執行緒池,一般為2*CPU核心數。 IO密集型任務CPU使用率並不高,因此可以讓CPU在等待IO的時候去處理別的任務,充分利用CPU時間。
- 混合型任務 可以將任務分成IO密集型和CPU密集型任務,然後分別用不同的執行緒池去處理。 只要分完之後兩個任務的執行時間相差不大,那麼就會比序列執行來的高效。 因為如果劃分之後兩個任務執行時間相差甚遠,那麼先執行完的任務就要等後執行完的任務,最終的時間仍然取決於後執行完的任務,而且還要加上任務拆分與合併的開銷,得不償失。