java線程池,阿里爲什麼不允許使用Executors?

java線程池,阿里爲什麼不允許使用Executors?
閱讀目錄

帶着問題
基礎
Java中的線程池
最後
回到目錄
帶着問題
阿里Java代碼規範爲什麼不允許使用Executors快速創建線程池?
下面的代碼輸出是什麼?
ThreadPoolExecutor executor = new ThreadPoolExecutor(

    1, //corePoolSize
    100, //maximumPoolSize
    100, //keepAliveTime
    TimeUnit.SECONDS, //unit
    new LinkedBlockingDeque<>(100));//workQueue

for (int i = 0; i < 5; i++) {

final int taskIndex = i;
executor.execute(() -> {
    System.out.println(taskIndex);
    try {
        Thread.sleep(Long.MAX_VALUE);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

}
A) 0 1 2 3 4 5

B) 0~5 順序不一致輸出5行

C) 0

回到目錄
基礎
什麼是線程池?
線程池可以通過池看出來是一個資源集,任何池的作用都大同小異,主要是用來減少資源創建、初始化的系統開銷。

創建線程很“貴”嗎?
是的。創建線程的代價是昂貴的。

我們都知道系統中的每個進程有自己獨立的內存空間,而被稱爲輕量級進程的線程也是需要的。

在JVM中默認一個線程需要使用256k~1M(取決於32位還是64位操作系統)的內存。(具體的數組我們不深究,因爲隨着JVM版本的變化這個默認值隨時可能發生變更,我們只需要知道線程是需要佔用內存的)

除了內存還有更多嗎?
許多文章會將上下文切換、CPU調度列入其中,這邊不將線程調度列入是因爲睡眠中的線程不會被調度(OS控制),如果不是睡眠中的線程那麼是一定需要被調度的。
但在JVM中除了創建時的內存消耗,還會給GC帶來壓力,如果頻繁創建線程那麼相對的GC的時候也需要回收對應的線程。

線程池的機制?
可以看到線程池是一種重複利用線程的技術,線程池的主要機制就是保留一定的線程數在沒有事情做的時候使之睡眠,當有活幹的時候拿一個線程去運行。
這些牽扯到線程池實現的具體策略。

還有哪些常見的池?
線程池
連接池(數據庫連接、TCP連接等)
BufferPool
......
回到目錄
Java中的線程池
UML圖(Java 8)
UML
可以看到真正的實現類有

ThreadPoolExecutor (1.5)
ForkJoinPool (1.7)
ScheduledThreadPoolExecutor (1.5)
今天我們主要談談 ThreadPoolExecutor 也是使用率較高的一個實現。

Executors提供的工廠方法
newCachedThreadPool (ThreadPoolExecutor)

創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。

newFixedThreadPool (ThreadPoolExecutor)

創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。

newSingleThreadExecutor (ThreadPoolExecutor)

創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。

newScheduledThreadPool (ScheduledThreadPoolExecutor)

創建一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

newSingleThreadScheduledExecutor (ScheduledThreadPoolExecutor)

創建一個單線程用於定時以及週期性執行任務的需求。

newWorkStealingPool (1.8 ForkJoinPool)

創建一個工作竊取

可以看到各種不同的工廠方法中使用的線程池實現類最終只有3個,對應關係如下:

工廠方法 實現類
newCachedThreadPool ThreadPoolExecutor
newFixedThreadPool ThreadPoolExecutor
newSingleThreadExecutor ThreadPoolExecutor
newScheduledThreadPool ScheduledThreadPoolExecutor
newSingleThreadScheduledExecutor ScheduledThreadPoolExecutor
newWorkStealingPool ForkJoinPool
ThreadPoolExecutor
首先我們看下 ThreadPoolExecutor 的完全構造函數

public ThreadPoolExecutor(int corePoolSize,

                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

corePoolSize

核心池大小,除非設置了 allowCoreThreadTimeOut 否則哪怕線程超過空閒時間,池中也要最少要保留這個數目的線程。

需要注意的是,corePoolSize所需的線程並不是立即創建的,需要在提交任務之後進行創建,所以如果有大量的緩存線程數可以先提交一個空任務讓線程池將線程先創建出來,從而提升後續的執行效率。

maximumPoolSize

允許的最大線程數。

keepAliveTime

空閒線程空閒存活時間,核心線程需要 allowCoreThreadTimeOut 爲true纔會退出。

unit

與 keepAliveTime 配合,設置 keepAliveTime 的單位,如:毫秒、秒。

workQueue

線程池中的任務隊列。上面提到線程池的主要作用是複用線程來處理任務,所以我們需要一個隊列來存放需要執行的任務,在使用池中的線程來處理這些任務,所以我們需要一個任務隊列。

threadFactory

當線程池判斷需要新的線程時通過線程工程創建線程。

handler

執行被阻止時的處理程序,線程池無法處理。這個與任務隊列相關,比如隊列中可以指定隊列大小,如果超過了這個大小該怎麼辦呢?JDK已經爲我們考慮到了,並提供了4個默認實現。

下列是JDK中默認攜帶的策略:
AbortPolicy (默認)

拋出 RejectedExecutionException 異常。

CallerRunsPolicy

調用當前線程池所在的線程去執行。

DiscardPolicy

直接丟棄當前任務。

DiscardOldestPolicy

將最舊的任務丟棄,將當前任務添加到隊列。

容易混淆的參數:corePoolSize maximumPoolSize workQueue

任務隊列、核心線程數、最大線程數的邏輯關係
當線程數小於核心線程數時,創建線程。
當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。
當線程數大於等於核心線程數,且任務隊列已滿
若線程數小於最大線程數,創建線程
若線程數等於最大線程數,調用拒絕執行處理程序(默認效果爲:拋出異常,拒絕任務)
那麼這三個參數推薦如何設置,有最優值嗎?
由於java對於協程的支持不友好,所以會大量依賴於線程池和線程。
從而這個值沒有最優推薦,需要根據業務需求情況來進行設置。
不同的需求類型可以創建多個不同的線程池來執行。

問題1:阿里開發規範爲什麼不允許Executors快速創建線程池?
p3c

參考地址:https://github.com/alibaba/p3c

可以看到原因很簡單

newSingleThreadExecutor
newFixedThreadPool
在 workQueue 參數直接 使用了 new LinkedBlockingQueue() 理論上可以無限添加任務到線程池。

public static ExecutorService newFixedThreadPool(int nThreads) {

return new ThreadPoolExecutor(nThreads, nThreads,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>();

}

public static ExecutorService newSingleThreadExecutor() {

return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));

}
如果提交到線程池的任務由問題,比如 sleep 永久,會造成內存泄漏,最終導致OOM。

同時 阿里還推薦自定義 threadFactory 設置線程名稱便於以後排查問題。

問題2:下面的代碼輸出是什麼?
應該選C。
雖然最大線程數有100但核心線程數爲1,任務隊列由100。
滿足了 '當線程數大於等於核心線程數,且任務隊列未滿時,將任務放入任務隊列。' 這個條件。
所以後續添加的任務都會被堵塞。

回到目錄
最後
關於 ThreadPoolExecutor 的邏輯在實際使用的時候會有點奇怪,因爲線程池中的線程並沒有超過最大線程數,有沒有一種可能當任務被堵塞很久的時候創建新的線程池來處理呢?

這邊推薦大家使用 newWorkStealingPool,也就是ForkJoinPool。採取了工作竊取的模式。
後續會跟大家一起聊聊 ForkJoinPool。

原文地址https://www.cnblogs.com/ants/p/11343657.html

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