手把手教你手動創(chuàng)建線程池

一蟋软、為什么要手動創(chuàng)建線程池?

我們之所以要手動創(chuàng)建線程池嗽桩,是因為 JDK 自帶的工具類所創(chuàng)建的線程池存在一定的弊端岳守,那究竟存在怎么樣的弊端呢?首先來回顧一下 JDK 中線程池框架的繼承關系:

JDK 線程池框架繼承關系圖

我們最常用的線程池實現類是ThreadPoolExecutor(紅框里的那個)碌冶,首先我們來看一下它最通用的構造方法:

/**
 * 各參數含義
 * corePoolSize    : 線程池中常駐的線程數量湿痢。核心線程數,默認情況下核心線程會一直存活扑庞,即使處于閑置狀態(tài)也不會 
 *                   受存活時間 keepAliveTime 的限制蒙袍,除非將 allowCoreThreadTimeOut 設置為 true。
 * maximumPoolSize : 線程池所能容納的最大線程數嫩挤。超過這個數的線程將被阻塞害幅。當任務隊列為沒有設置大小的
 *                         LinkedBlockingQueue時,這個值無效岂昭。
 * keepAliveTime   : 當線程數量多于 corePoolSize 時以现,空閑線程的存活時長,超過這個時間就會被回收
 * unit            : keepAliveTime 的時間單位
 * workQueue       : 存放待處理任務的隊列
 * threadFactory   : 線程工廠
 * handler         : 拒絕策略约啊,拒絕無法接收添加的任務
 */
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                            RejectedExecutionHandler handler) { ... ... }

使用 JDK 自帶的 Executors工具類 (圖中藍色框中的那個邑遏,這是獨立于線程池繼承關系圖的工具類,類似于 Collections 和 Arrays) 可以直接創(chuàng)建以下種類的線程池:

  1. 線程數量固定的線程池恰矩,此方法返回 ThreadPoolExecutor

    public static ExecutorService newFixedThreadPool(int nThreads) {... ...}
    
  2. 單線程線程池记盒,此方法返回 ThreadPoolExecutor

    public static ExecutorService newSingleThreadExecutor() {... ...}
    
  3. 可緩存線程的線程池,此方法返回 ThreadPoolExecutor

    public static ExecutorService newCachedThreadPool() {... ...}
    
  4. 執(zhí)行定時任務的線程池外傅,此方法返回 ScheduledThreadPoolExecutor

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {... ...}
    
  5. 可以拆分執(zhí)行子任務的線程池纪吮,此方法返回 ForkJoinPool

    public static ExecutorService newWorkStealingPool() {... ...}
    

JDK 自帶工具類創(chuàng)建的線程池存在的問題

直接使用這些線程池雖然很方便,但是存在兩個比較大的問題:

  1. 有的線程池可以無限添加任務或線程萎胰,容易導致 OOM碾盟;

    就拿我們最常用FixedThreadPoolCachedThreadPool來說,前者的詳細創(chuàng)建方法如下:

    public static ExecutorService newFixedThreadPool(int nThreads) {
     return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>());
    }
    

    可見其任務隊列用的是LinkedBlockingQueue,且沒有指定容量技竟,相當于無界隊列冰肴,這種情況下就可以添加大量的任務,甚至達到Integer.MAX_VALUE的數量級,如果任務大量堆積熙尉,可能會導致 OOM联逻。

    CachedThreadPool的創(chuàng)建方法如下:

    public static ExecutorService newCachedThreadPool() {
     return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
    }
    

    這個雖然使用了有界隊列SynchronousQueue,但是最大線程數設置成了Integer.MAX_VALUE检痰,這就意味著可以創(chuàng)建大量的線程包归,也有可能導致 OOM。

  2. 還有一個問題就是這些線程池的線程都是使用 JDK 自帶的線程工廠 (ThreadFactory)創(chuàng)建的攀细,線程名稱都是類似pool-1-thread-1的形式,第一個數字是線程池編號爱态,第二個數字是線程編號谭贪,這樣很不利于系統(tǒng)異常時排查問題。

如果你安裝了“阿里編碼規(guī)約”的插件锦担,在使用Executors創(chuàng)建線程池時會出現以下警告信息:

阿里編碼規(guī)約的警告信息

為避免這些問題俭识,我們最好還是手動創(chuàng)建線程池。

二洞渔、 如何手動創(chuàng)建線程池

2.1 定制線程數量

首先要說明一點套媚,定制線程池的線程數并不是多么高深的學問,也不是說一旦線程數設定不合理磁椒,你的程序就無法運行堤瘤,而是要盡量避免以下兩種極端條件:

  1. 線程數量過大

    這會導致過多的線程競爭稀缺的 CPU 和內存資源。CPU 核心的數量和計算能力是有限的浆熔,在分配不到 CPU 執(zhí)行時間的情況下本辐,線程只能處于空閑狀態(tài)。而在JVM 中医增,線程本身也是對象慎皱,也會占用內存,過多的空閑線程自然會浪費寶貴的內存空間叶骨。

  2. 線程數量過小

    線程池存在的意義茫多,或者說并發(fā)編程的意義就是為了“壓榨”計算機的運算能力,說白了就是別讓 CPU 閑著忽刽。如果線程數量比 CPU 核心數量還小的話天揖,那么必定有 CPU 核心將處于空閑狀態(tài),這是極大的浪費跪帝。

所以在實際開發(fā)中我們需要根據實際的業(yè)務場景合理設定線程池的線程數量宝剖,那又如何分析業(yè)務場景呢?我們的業(yè)務場景大致可以分為以下兩大類:

  1. CPU (計算)密集型

    這種場景需要大量的 CPU 計算歉甚,比如加密万细、計算 hash 等,最佳線程數為 (CPU 核心數 + 1)。比如8核 CPU赖钞,可以把線程數設置為 9腰素,這樣就足夠了,因為在 CPU 密集型的場景中雪营,每個線程都會在比較大的負荷下工作弓千,很少出現空閑的情況,正好每個線程對應一個 CPU 核心献起,然后不停地工作洋访,這樣就實現了最優(yōu)利用率。多出的一個線程起到了備胎的作用谴餐,在其他線程意外中斷時頂替上去姻政,確保 CPU 不中斷工作。其實也大可不必這么死板岂嗓,線程數量設置為 CPU 核心數的 1 到 2 倍都是可以接受的汁展。

  2. I/O 密集型

    比如讀寫數據庫,讀寫文件或者網絡讀寫等場景厌殉。各種 I/O 設備 (比如磁盤)的速度是遠低于 CPU 執(zhí)行速度的食绿,所以在 I/O 密集型的場景下,線程大部分時間都在等待資源而非 CPU 時間片公罕,這樣的話一個 CPU 核心就可以應付很多線程了器紧,也就可以把線程數量設置大一點。線程具體數量的計算方法可以參考 Brain Goetz 的建議:

    假設有以下變量:

    • Nthreads = 線程數量
    • Ncpu = CPU 核心數
    • Ucpu = 期望的CPU 的使用率 楼眷,因為 CPU 可能還要執(zhí)行其他任務
    • W = 線程的平均等待資源時間
    • C = 線程平均使用 CPU 的計算時間
    • W / C = 線程等待時間與計算時間的比率

    這樣為了讓 CPU 達到期望的使用率品洛,最優(yōu)的線程數量計算公式如下:

    Nthreads = Ncpu * Ucpu* ( 1 + W / C )

CPU 核心數可以通過以下方法獲取:

int N_CPUS = Runtime.getRuntime().availableProcessors();

當然摩桶,除了 CPU桥状,線程數量還會受到很多其他因素的影響,比如內存和數據庫連接等硝清,需要具體問題具體分析辅斟。

2.2 使用可自定義線程名稱的線程工廠

這個就簡單多了,可以借助大名鼎鼎的谷歌開源工具庫 Guava芦拿,首先引入如下依賴:

<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>28.2-jre</version>
</dependency>

然后我就可以使用其提供的ThreadFactoryBuilder類來創(chuàng)建線程工廠了士飒,Demo 如下:

public class ThreadPoolDemo {

    // 線程數
    public static final int THREAD_POOL_SIZE = 16;

    public static void main(String[] args) throws InterruptedException {
        // 使用 ThreadFactoryBuilder 創(chuàng)建自定義線程名稱的 ThreadFactory
        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
                .setNameFormat("hyn-demo-pool-%d").build();
        
        // 創(chuàng)建線程池,其中任務隊列需要結合實際情況設置合理的容量
        ThreadPoolExecutor executor = new ThreadPoolExecutor(THREAD_POOL_SIZE,
                THREAD_POOL_SIZE,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(1024),
                namedThreadFactory,
                new ThreadPoolExecutor.AbortPolicy());
        
        // 新建 1000 個任務蔗崎,每個任務是打印當前線程名稱
        for (int i = 0; i < 1000; i++) {
            executor.execute(() -> System.out.println(Thread.currentThread().getName()));
        }
        // 優(yōu)雅關閉線程池
        executor.shutdown();
        executor.awaitTermination(1000L, TimeUnit.SECONDS);
        // 任務執(zhí)行完畢后打印"Done"
        System.out.println("Done");
    }
}

控制臺打印結果如下:

... ...
hyn-demo-pool-2
hyn-demo-pool-6
hyn-demo-pool-13
hyn-demo-pool-12
hyn-demo-pool-15
Done

可見這樣的線程名稱相比pool-1-thread-1更有辨識度酵幕,可以為不同用途的線程池設定不同的名稱,便于系統(tǒng)出故障時排查問題缓苛。

三芳撒、總結

本文為大家介紹了手動創(chuàng)建線程池的詳細方法,不過這些都是理論性的內容,而多線程編程是非常注重實踐的一門學問笔刹,在實際生產環(huán)境中要綜合考慮各種因素并不斷嘗試芥备,才能實現最佳實踐。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末舌菜,一起剝皮案震驚了整個濱河市萌壳,隨后出現的幾起案子,更是在濱河造成了極大的恐慌日月,老刑警劉巖袱瓮,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異爱咬,居然都是意外死亡尺借,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門台颠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褐望,“玉大人勒庄,你說我怎么就攤上這事串前。” “怎么了实蔽?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵荡碾,是天一觀的道長。 經常有香客問我局装,道長坛吁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任铐尚,我火速辦了婚禮拨脉,結果婚禮上,老公的妹妹穿的比我還像新娘宣增。我一直安慰自己玫膀,他們只是感情好,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布爹脾。 她就那樣靜靜地躺著帖旨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪灵妨。 梳的紋絲不亂的頭發(fā)上解阅,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音泌霍,去河邊找鬼货抄。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的碉熄。 我是一名探鬼主播桨武,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锈津!你這毒婦竟也來了呀酸?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤琼梆,失蹤者是張志新(化名)和其女友劉穎性誉,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體茎杂,經...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡错览,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了煌往。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倾哺。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖刽脖,靈堂內的尸體忽然破棺而出羞海,到底是詐尸還是另有隱情,我是刑警寧澤曲管,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布却邓,位于F島的核電站,受9級特大地震影響院水,放射性物質發(fā)生泄漏腊徙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一檬某、第九天 我趴在偏房一處隱蔽的房頂上張望撬腾。 院中可真熱鬧,春花似錦恢恼、人聲如沸民傻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽饰潜。三九已至,卻和暖如春和簸,著一層夾襖步出監(jiān)牢的瞬間彭雾,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工锁保, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留薯酝,地道東北人半沽。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像吴菠,于是被迫代替她去往敵國和親者填。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內容