爲什麼阿里內部不允許用Executors創建線程池?

來源:cnblogs.com/zjfjava/p/11227456.html

1. 通過Executors創建線程池的弊端

在創建線程池的時候,大部分人還是會選擇使用Executors去創建。

下面是創建定長線程池(FixedThreadPool)的一個例子,嚴格來說,當使用如下代碼創建線程池時,是不符合編程規範的。

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);

原因在於:(摘自阿里編碼規約)

線程池不允許使用Executors去創建,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。說明:Executors各個方法的弊端:

1)newFixedThreadPool和newSingleThreadExecutor: 主要問題是堆積的請求處理隊列可能會耗費非常大的內存,甚至OOM。

2)newCachedThreadPool和newScheduledThreadPool:   主要問題是線程數最大數是Integer.MAX_VALUE,可能會創建數量非常多的線程,甚至OOM。

2. 通過ThreadPoolExecutor創建線程池

所以,針對上面的不規範代碼,重構爲通過ThreadPoolExecutor創建線程池的方式。

    /**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters and default thread factory.
*
* @param corePoolSize the number of threads to keep in the pool, even
* if they are idle, unless {@code allowCoreThreadTimeOut} is set
* @param maximumPoolSize the maximum number of threads to allow in the
* pool
* @param keepAliveTime when the number of threads is greater than
* the core, this is the maximum time that excess idle threads
* will wait for new tasks before terminating.
* @param unit the time unit for the {@code keepAliveTime} argument
* @param workQueue the queue to use for holding tasks before they are
* executed. This queue will hold only the {@code Runnable}
* tasks submitted by the {@code execute} method.
* @param handler the handler to use when execution is blocked
* because the thread bounds and queue capacities are reached
* @throws IllegalArgumentException if one of the following holds:<br>
* {@code corePoolSize < 0}<br>
* {@code keepAliveTime < 0}<br>
* {@code maximumPoolSize <= 0}<br>
* {@code maximumPoolSize < corePoolSize}
* @throws NullPointerException if {@code workQueue}
* or {@code handler} is null
*/

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
{
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}

ThreadPoolExecutor 是線程池的核心實現。線程的創建和終止需要很大的開銷,線程池中預先提供了指定數量的可重用線程,所以使用線程池會節省系統資源,並且每個線程池都維護了一些基礎的數據統計,方便線程的管理和監控。

3. ThreadPoolExecutor參數解釋

下面是對其參數的解釋,在創建線程池時需根據自己的情況來合理設置線程池。

corePoolSize & maximumPoolSize

核心線程數(corePoolSize)和最大線程數(maximumPoolSize)是線程池中非常重要的兩個概念,希望同學們能夠掌握。當一個新任務被提交到池中,如果當前運行線程小於核心線程數(corePoolSize),即使當前有空閒線程,也會新建一個線程來處理新提交的任務;如果當前運行線程數大於核心線程數(corePoolSize)並小於最大線程數(maximumPoolSize),只有當等待隊列已滿的情況下才會新建線程。

keepAliveTime & unit

keepAliveTime 爲超過 corePoolSize 線程數量的線程最大空閒時間,unit 爲時間單位。

等待隊列

任何阻塞隊列(BlockingQueue)都可以用來轉移或保存提交的任務,線程池大小和阻塞隊列相互約束線程池:

  1. 如果運行線程數小於 corePoolSize,提交新任務時就會新建一個線程來運行;
  2. 如果運行線程數大於或等於 corePoolSize,新提交的任務就會入列等待;如果隊列已滿,並且運行線程數小於 maximumPoolSize,也將會新建一個線程來運行;
  3. 如果線程數大於 maximumPoolSize,新提交的任務將會根據 拒絕策略來處理。

下面來看一下三種通用的入隊策略:

  1. 直接傳遞:通過 SynchronousQueue 直接把任務傳遞給線程。如果當前沒可用線程,嘗試入隊操作會失敗,然後再創建一個新的線程。當處理可能具有內部依賴性的請求時,該策略會避免請求被鎖定。直接傳遞通常需要無界的最大線程數(maximumPoolSize),避免拒絕新提交的任務。當任務持續到達的平均速度超過可處理的速度時,可能導致線程的無限增長。
  2. 無界隊列:使用無界隊列(如 LinkedBlockingQueue)作爲等待隊列,當所有的核心線程都在處理任務時, 新提交的任務都會進入隊列等待。因此,不會有大於 corePoolSize 的線程會被創建(maximumPoolSize 也將失去作用)。這種策略適合每個任務都完全獨立於其他任務的情況;例如網站服務器。這種類型的等待隊列可以使瞬間爆發的高頻請求變得平滑。當任務持續到達的平均速度超過可處理速度時,可能導致等待隊列無限增長。
  3. 有界隊列:當使用有限的最大線程數時,有界隊列(如 ArrayBlockingQueue)可以防止資源耗盡,但是難以調整和控制。隊列大小和線程池大小可以相互作用:使用大的隊列和小的線程數可以減少CPU使用率、系統資源和上下文切換的開銷,但是會導致吞吐量變低,如果任務頻繁地阻塞(例如被I/O限制),系統就能爲更多的線程調度執行時間。使用小的隊列通常需要更多的線程數,這樣可以最大化CPU使用率,但可能會需要更大的調度開銷,從而降低吞吐量。

拒絕策略

當線程池已經關閉或達到飽和(最大線程和隊列都已滿)狀態時,新提交的任務將會被拒絕。ThreadPoolExecutor 定義了四種拒絕策略:

  1. AbortPolicy:默認策略,在需要拒絕任務時拋出RejectedExecutionException;
  2. CallerRunsPolicy:直接在 execute 方法的調用線程中運行被拒絕的任務,如果線程池已經關閉,任務將被丟棄;
  3. DiscardPolicy:直接丟棄任務;
  4. DiscardOldestPolicy:丟棄隊列中等待時間最長的任務,並執行當前提交的任務,如果線程池已經關閉,任務將被丟棄。

我們也可以自定義拒絕策略,只需要實現 RejectedExecutionHandler;需要注意的是,拒絕策略的運行需要指定線程池和隊列的容量。

4. ThreadPoolExecutor創建線程方式

通過下面的demo來了解ThreadPoolExecutor創建線程的過程。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
* 測試ThreadPoolExecutor對線程的執行順序
**/

public class ThreadPoolSerialTest {
public static void main(String[] args) {
//核心線程數
int corePoolSize = 3;
//最大線程數
int maximumPoolSize = 6;
//超過 corePoolSize 線程數量的線程最大空閒時間
long keepAliveTime = 2;
//以秒爲時間單位
TimeUnit unit = TimeUnit.SECONDS;
//創建工作隊列,用於存放提交的等待執行任務
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
ThreadPoolExecutor threadPoolExecutor = null;
try {
//創建線程池
threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
new ThreadPoolExecutor.AbortPolicy());

//循環提交任務
for (int i = 0; i < 8; i++) {
//提交任務的索引
final int index = (i + 1);
threadPoolExecutor.submit(() -> {
//線程打印輸出
System.out.println("大家好,我是線程:" + index);
try {
//模擬線程執行時間,10s
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//每個任務提交後休眠500ms再提交下一個任務,用於保證提交順序
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
threadPoolExecutor.shutdown();
}
}
}

執行結果:

這裏描述一下執行的流程:

  • 首先通過 ThreadPoolExecutor 構造函數創建線程池;
  • 執行 for 循環,提交 8 個任務(恰好等於maximumPoolSize[最大線程數] + capacity[隊列大小]);
  • 通過 threadPoolExecutor.submit 提交 Runnable 接口實現的執行任務;
  • 提交第1個任務時,由於當前線程池中正在執行的任務爲 0 ,小於 3(corePoolSize 指定),所以會創建一個線程用來執行提交的任務1;
  • 提交第 2, 3 個任務的時候,由於當前線程池中正在執行的任務數量小於等於 3 (corePoolSize 指定),所以會爲每一個提交的任務創建一個線程來執行任務;
  • 當提交第4個任務的時候,由於當前正在執行的任務數量爲 3 (因爲每個線程任務執行時間爲10s,所以提交第4個任務的時候,前面3個線程都還在執行中),此時會將第4個任務存放到 workQueue 隊列中等待執行;
  • 由於 workQueue 隊列的大小爲 2 ,所以該隊列中也就只能保存 2 個等待執行的任務,所以第5個任務也會保存到任務隊列中;
  • 當提交第6個任務的時候,因爲當前線程池正在執行的任務數量爲3,workQueue 隊列中存儲的任務數量也滿了,這時會判斷當前線程池中正在執行的任務的數量是否小於6(maximumPoolSize指定);
  • 如果小於 6 ,那麼就會新創建一個線程來執行提交的任務 6;
  • 執行第7,8個任務的時候,也要判斷當前線程池中正在執行的任務數是否小於6(maximumPoolSize指定),如果小於6,那麼也會立即新建線程來執行這些提交的任務;
  • 此時,6個任務都已經提交完畢,那 workQueue 隊列中的等待 任務4 和 任務5 什麼時候執行呢?
  • 當任務1執行完畢後(10s後),執行任務1的線程並沒有被銷燬掉,而是獲取 workQueue 中的任務4來執行;
  • 當任務2執行完畢後,執行任務2的線程也沒有被銷燬,而是獲取 workQueue 中的任務5來執行;

通過上面流程的分析,也就知道了之前案例的輸出結果的原因。其實,線程池中會線程執行完畢後,並不會被立刻銷燬,線程池中會保留 corePoolSize 數量的線程,當 workQueue 隊列中存在任務或者有新提交任務時,那麼會通過線程池中已有的線程來執行任務,避免了頻繁的線程創建與銷燬,而大於 corePoolSize 小於等於 maximumPoolSize 創建的線程,則會在空閒指定時間(keepAliveTime)後進行回收。

5. ThreadPoolExecutor拒絕策略

在上面的測試中,我設置的執行線程總數恰好等於maximumPoolSize[最大線程數] + capacity[隊列大小],因此沒有出現需要執行拒絕策略的情況,因此在這裏,我再增加一個線程,提交9個任務,來演示不同的拒絕策略。

AbortPolicy

CallerRunsPolicy

DiscardPolicy

DiscardOldestPolicy
參考: www.jianshu.com/p/6f82b738ac58


本文分享自微信公衆號 - Java中文社羣(javacn666)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章