Categories
程式開發

Java並發編程系列——線程池


之前寫了線程和鎖,例子中採用直接創建線程的方式,這種方式做示例可以,但在實際生產環境中比較少用,通常會使用線程池。

使用線程池有一些明顯的好處,可以考慮我們使用連接池的情形,不難想像。使用線程池可以免去我們手動創建和銷毀線程的工作,節省這部分資源的消耗,提高響應速度,同時線程由線程池維護,也提高了線程的可管理性。

JDK中默認實現了多種線程池,如FixedThreadPool,SingleThreadExecutor,CachedThreadPool,ScheduledThreadPool,SingleThreadScheduledExecutor,WorkStealingPool。 ForkJoinPool也是線程池的一種,通常我們單獨討論,之前的文章有所介紹。

線程工廠

線程池的創建方法使用線程池的工廠類Executors,調用相應的方法創建相應的線程池。創建線程池的工作即實例化ThreadPoolExecutor,所以有必要簡單看下ThreadPoolExecutor。

ThreadPoolExecutor的構造參數:

/**
* Creates a new {@code ThreadPoolExecutor} with the given initial
* parameters.
*
* @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 threadFactory the factory to use when the executor
* creates a new thread
* @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:
* {@code corePoolSize < 0}
* {@code keepAliveTime < 0}
* {@code maximumPoolSize <= 0}
* {@code maximumPoolSize < corePoolSize} * @throws NullPointerException if {@code workQueue} * or {@code threadFactory} or {@code handler} is null */ public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

對如上參數簡單說明:

corePoolSize:線程池中的核心線程數。也就是線程池會長期保持的線程數。當通過線程池執行任務時,如果當前的線程數小於corePoolSize,則創建新線程,如果當前線程數已經為corePoolSize,則任務進入工作隊列。如果希望一次性創建出核心線程,調用prestartAllCoreThreads()。

maximumPoolSize:池中允許的最大線程數。當核心線程數滿,並且阻塞隊列也滿了,此時如果池中的線程數仍小於maximumPoolSize,則會創建新的線程。

keepAliveTime:空閒線程存活的時間。僅當池中線程數大於corePoolSize時有效,也就是在上述maximumPoolSize所講條件觸發創建線程後,使得池中線程大於核心線程數後,才會根據該條件來銷毀線程。

unit:時間單位。

workQueue:保存尚未執行的任務的阻塞隊列。

threadFactory:創建線程的工廠。

handler:飽和策略。也就是阻塞隊列滿了之後的處理方式。飽和策略有四種,AbortPolicy(拋出異常),CallerRunsPolicy(由調用線程直接執行,如果調用線程已經銷毀則丟棄),DiscardOldestPolicy(丟棄最早的任務,即隊列頭部的任務),DiscardPolicy(直接丟棄) 。

任務的執行

用線程池執行任務有兩種方式,execute和submit,execute無返回值,submit返回Future,而Future可以返回結果和接收異常。根據需要使用具體的方法。

線程池的停止

關閉線程池使用shutdown()和shutdownNow()。使用shutdown()時,尚未被執行的任務將不再執行,而已經在執行的任務將繼續。使用shutdownNow()時,尚未被執行的任務將不再執行,並且會嘗試停止正在運行的任務。

接下來簡單介紹下幾個常見線程池。

FixedThreadPool:

corePoolSize等於maximumPoolSize,阻塞隊列使用了LinkedBlockingQueue,但並未初始化其容量,可以認為相當於使用了無界隊列(為什麼說到無界,文章最後會提到)。適用於對服務器負載有嚴格控制的場景。

SingleThreadExecutor:

corePoolSize等於maximumPoolSize等於1,阻塞隊列同樣使用了未初始化容量的LinkedBlockingQueue,為無界隊列。適用於對任務執行順序有要求的場景。

CachedThreadPool:

corePoolSize為0,maximumPoolSize為Integer.MAX_VALUE,使用的隊列為SynchronousQueue,同樣為無界。該線程池會根據需要創建新線程,適用於執行時間非常短的數量較多的異步任務。

ScheduledThreadPool:

其maximumPoolSize為Integer.MAX_VALUE,隊列使用了DelayedWorkQueue,同樣為無界。適用於需要定期執行任務的場景。

SingleThreadScheduledExecutor

corePoolSize為1,隊列同樣使用了DelayedWorkQueue,為無界。適用於需要定期按順序執行任務的場景。

WorkStealingPool:

工作密取隊列,內部使用了ForkJoinPool,但使用了默認工廠創建,同樣為無界形式。

線程池使用示例

通過一段代碼簡單看下線程的使用,以SingleThreadExecutor為例。

public class ShowSingleExecutor {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.execute(() -> {
System.out.println("executed 1 by single thread");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});

executorService.execute(() -> {
System.out.println("executed 2 by single thread");
}
);
executorService.shutdown();
}
}

該示例將演示使用單線程線程池,其第一個任務執行5秒後才會執行第二個任務。

其他線程池文章中鑑於篇幅不再舉例。

自定義線程池

最後說一下之前提到的無界問題。無界意味著如果出現處理不夠及時的情況時,任務會逐漸堆積,而造成服務不可用或服務崩潰。所以在實際使用中通常需要自定義ThreadPoolExecutor,並在內部使用有界隊列的方式或通過其他手段達到類似有界的效果。對於隊列滿時的飽和策略除了文中介紹的四種實現,同樣可以根據實際情況自定義。

本系列其他文章:

Java並發編程系列——鎖順序"

Java並發編程系列——鎖"

Java並發編程系列——常用並發工具類"

Java並發編程系列——Fork-Join"

Java並發編程系列——線程的等待與喚醒"

Java並發編程系列插曲——對象的內存結構"

Java並發編程系列——線程"