Java線程池
最近生產環境的線程池出現了隊列中的線程阻塞過多導致服務器不可用的情況,所以藉此機會,仔細研究了一波線程池。在學習了大神的文章後,做了如下的總結和整理。
使用線程池的優勢
第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制的創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一的分配,調優和監控。
線程池的底層工作原理
從上圖我們可以看出,當提交一個新任務到線程池時,線程池的處理流程如下:
首先線程池判斷核心線程池是否已滿?沒滿,則創建一個工作線程來執行任務。滿了,則進入下個流程。
其次線程池判斷工作隊列是否已滿?沒滿,則將新提交的任務存儲在工作隊列裏。滿了,則進入下個流程。
最後線程池判斷整個線程池是否已滿?沒滿,則創建一個新的工作線程來執行任務,滿了,則交給飽和策略來處理這個任務。
ThreadPoolExecutor 執行 execute 方法分下面四種情況。
1)當前運行的線程少於 corePoolSize,則創建新線程來執行任務(注意,執行這一步驟需要獲取全局鎖)。
2)如果運行的線程大於等於corePoolSize,則將任務加入 BlockingQueue。
3)如果無法將任務加入 BlockingQueue 中(說明隊列已滿),則創建新的線程來處理任務(注意,執行這一步驟需要獲取全局鎖)。
4)如果創建新線程將使當前運行的線程超出 maximumPoolSize ,任務將被拒絕,並調用 RejectedExecutionHandler.rejectedExecution() 方法。
線程池的幾個重要參數介紹
ThreadPoolExecutor的構造方法如下:
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:線程池中的常駐核心線程數。如果調用了線程池的prestartAllCoreThreads 方法,線程池會提前創建並啓動所有基本線程。
- maximumPoolSize:線程池允許創建的最大線程數,此值必須大於等於1。
- workQueue:等待隊列,用於存放被提交但尚未執行的任務。
- keepAliveTime:多餘的空閒線程的存活時間。當前線程池的數量超過 corePoolSize 時,當空閒時間達到 keepAliveTime 值時,多餘空閒線程會被銷燬直到只剩下 corePoolSize 個線程爲止。
- unit :keepAliveTime 的單位。可選的單位有天(DAYS),小時(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
- threadFactory:用於設置創建線程的工廠,可通過該工廠給線程重命名等等。一般使用默認的即可。
- handler:拒絕策略。當隊列滿了,並且工作線程數量大於等於線程池的最大線程數(maximumPoolSize)時,如何拒絕請求執行的 runnable 的策略。
線程池的幾種等待隊列
- ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按 FIFO(先進先出)原則對元素進行排序。
- LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按 FIFO (先進先出) 排序元素,吞吐量通常要高於 ArrayBlockingQueue。靜態工廠方法 Executors.newFixedThreadPool() 使用了這個隊列。
- SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個線程調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於LinkedBlockingQueue,靜態工廠方法 Executors.newCachedThreadPool 使用了這個隊列。
- PriorityBlockingQueue:一個具有優先級的無限阻塞隊列。
線程池的幾種拒絕策略
-
AbortPolicy:直接拋出 RejectedExecutionException 異常,這是默認策略。
-
CallerRunsPolicy:用調用者所在的線程來執行任務。
-
DiscardOldestPolicy:丟棄阻塞隊列中最靠前(等待最久)的任務,並執行當前任務(ThreadPoolExecutor.execute(runnable))。
-
DiscardPolicy:直接丟棄任務。
線程池怎麼使用
1.使用 Executors 工具類來產生一個線程池。代碼如下:
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
- newFixedThreadPool 創建一個定長線程池,可控制線程最大併發數,超出的線程會在隊列中等待。
- newSingleThreadExecutor 創建一個單線程化的線程池,它只會用唯一的工作線程來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先級)執行。
- newCachedThreadPool創建一個可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閒線程,若無可回收,則新建線程。如果工作線程空閒 60 秒沒有被使用,會自動關閉。
- newScheduledThreadPool 創建一個定長線程池,支持定時及週期性任務執行。
2.通過 ThreadPoolExecutor 創建一個線程池。代碼如下:
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
通過以上兩種方式都可以得到一個線程池,那麼我們日常開發中應該使用哪種呢?我們建議使用第 2 種方式來創建線程池。
Executors 返回的線程池對象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
2) CachedThreadPool 和 ScheduledThreadPool:
允許的創建線程數量爲 Integer.MAX_VALUE, 可能會創建大量的線程,從而導致 OOM。
《阿里巴巴Java開發手冊》原文如下:
線程池的關閉
我們可以通過調用線程池的 shutdown 或 shutdownNow 方法來關閉線程池,但是它們的實現原理不同。
shutdown : 線程池拒接收新提交的任務,同時等待線程池⾥的任務執行完畢後關閉線程池。 原理是隻是將線程池的狀態設置成 SHUTDOWN 狀態,然後中斷所有沒有正在執行任務的線程。
shutdownNow :線程池拒接收新提交的任務,同時⽴刻關閉線程池,線程池里的任務不再執行。原理是遍歷線程池中的工作線程,然後逐個調用線程的 interrupt 方法來中斷線程,所以無法響應中斷的任務可能永遠無法終止。shutdownNow 會首先將線程池的狀態設置成 STOP,然後嘗試停止所有的正在執行或暫停任務的線程,並返回等待執行任務的列表。
只要調用了這兩個關閉方法的其中一個,isShutdown 方法就會返回 true。當所有的任務都已關閉後,才表示線程池關閉成功,這時調用 isTerminaed 方法會返回 true。至於我們應該調用哪一種方法來關閉線程池,應該由提交到線程池的任務特性決定,通常調用 shutdown 來關閉線程池,如果任務不一定要執行完,則可以調用 shutdownNow。
如何合理配置線程池的線程數
要想合理的配置線程池,就必須首先分析任務特性,可以從以下幾個角度來進行分析:
- 任務的性質:CPU密集型任務,IO密集型任務和混合型任務。
- 任務的優先級:高,中和低。
- 任務的執行時間:長,中和短。
- 任務的依賴性:是否依賴其他系統資源,如數據庫連接。
CPU 密集型任務配置儘可能少的線程數量,如配置 Ncpu+1 個線程的線程池。
IO 密集型任務則由於需要等待 IO 操作,線程並不是一直在執行任務,則配置儘可能多的線程,如 2*Ncpu。
混合型的任務,如果可以拆分,則將其拆分成一個 CPU 密集型任務和一個 IO 密集型任務,只要這兩個任務執行的時間相差不是太大,那麼分解後執行的吞吐率要高於串行執行的吞吐率,如果這兩個任務執行時間相差太大,則沒必要進行分解。
線程池的監控
通過線程池提供的參數進行監控。線程池裏有一些屬性在監控線程池的時候可以使用。
- taskCount:線程池需要執行的任務數量。
- completedTaskCount:線程池在運行過程中已完成的任務數量。小於或等於 taskCount。
- largestPoolSize:線程池曾經創建過的最大線程數量。通過這個數據可以知道線程池是否滿過。如等於線程池的最大大小,則表示線程池曾經滿了。
- getPoolSize: 線程池的線程數量。如果線程池不銷燬的話,池裏的線程不會自動銷燬,所以這個大小隻增不減。
- getActiveCount:獲取活動的線程數。
最後,你還可以通過擴展線程池進行監控。通過繼承線程池並重寫線程池的beforeExecute,afterExecute和terminated方法,我們可以在任務執行前,執行後和線程池關閉前幹一些事情。如監控任務的平均執行時間,最大執行時間和最小執行時間等。這幾個方法在線程池裏是空方法。