深入淺出Java多執行緒(十二):執行緒池

解码猿發表於2024-03-13

引言


大家好,我是你們的老夥計秀才!今天帶來的是[深入淺出Java多執行緒]系列的第十二篇內容:執行緒池。大家覺得有用請點贊,喜歡請關注!秀才在此謝過大家了!!!

在現代軟體開發中,多執行緒程式設計已經成為應對高併發、高效能場景的必備技術。隨著計算機硬體的發展,尤其是多核CPU的普及,利用多執行緒能夠充分利用系統資源,提升程式執行效率和響應速度。然而,在直接使用原生執行緒建立與銷燬的過程中,我們往往會遇到一些難以忽視的問題:

首先,執行緒的建立和銷燬並非無成本操作。作業系統需要分配記憶體空間給執行緒棧,以及為執行緒排程維護上下文切換等資訊,頻繁地建立和銷燬執行緒會導致系統資源被大量消耗。尤其在處理短生命週期任務時,這種開銷可能遠大於實際業務邏輯執行的耗時。

其次,過多的併發執行緒可能會引發資源競爭問題,導致伺服器過載甚至崩潰。當併發數量不受控制時,系統記憶體、CPU資源乃至檔案控制代碼等關鍵資源都可能因過度消耗而達到瓶頸,從而影響整個系統的穩定性與效能。

再者,對執行緒進行分散管理會增加程式碼複雜度和出錯風險。沒有一個統一管理和協調的機制,程式設計師很難準確預測和控制多執行緒間的互動行為,例如同步問題、死鎖現象及資源爭搶等問題,這些問題都會降低程式的質量和可維護性。

因此,Java提供了強大的執行緒池機制,透過Executor介面及其核心實現類ThreadPoolExecutor來解決上述挑戰。執行緒池能有效地複用已存在的執行緒,避免了頻繁建立和銷燬執行緒帶來的開銷;同時,它允許我們預設並動態調整執行緒池大小以控制併發執行的任務數,確保系統資源合理利用而不至於過載。此外,執行緒池還能對執行緒進行統一管理和異常處理,簡化了多執行緒程式設計的複雜性。

例如,我們可以直觀地透過Java程式碼例項展示執行緒池的優勢:

ExecutorService executor = Executors.newFixedThreadPool(10); // 建立固定大小的執行緒池

for (int i = 0; i < 1000; i++) {
Runnable task = new Task(i); // 假設有Task是一個實現了Runnable介面的任務類
executor.execute(task); // 將任務提交到執行緒池中執行
}

executor.shutdown(); // 當所有任務提交完畢後,關閉執行緒池,等待所有任務執行完成

透過上述程式碼片段可以看到,執行緒池負責管理這些待執行的任務,並根據預先設定的核心執行緒數來高效地排程執行,極大地提升了程式設計效率和系統的執行效能。接下來的文章將深入剖析Java多執行緒之執行緒池原理,從執行緒池構造方法引數的意義,到其內部狀態機設計和任務處理流程,再到執行緒複用的具體實現細節,全面揭示這一重要元件的工作機制和應用場景。

為什麼要使用執行緒池


在多執行緒程式設計中,使用執行緒池(Thread Pool)是提高程式併發處理能力和資源利用率的關鍵技術。以下是採用執行緒池的三個主要原因:

減少系統資源消耗 建立和銷燬執行緒是一項昂貴的操作,涉及到記憶體分配、上下文切換等系統資源的大量消耗。頻繁建立和銷燬執行緒可能導致效能瓶頸。執行緒池透過預先建立並維護一定數量的執行緒來複用這些執行緒資源,當有新任務提交時直接將任務分配給空閒執行緒執行,從而避免了頻繁建立執行緒的成本。例如,在Java中,透過ExecutorService介面及其實現類ThreadPoolExecutor可以方便地建立一個執行緒池,並利用其管理執行緒生命週期,如下所示:

ExecutorService executor = Executors.newFixedThreadPool(5); // 建立包含5個核心執行緒的執行緒池

控制併發數量以防止伺服器過載 在高併發場景下,如果不加限制地建立執行緒,可能會導致併發數量過多,超出系統承受能力,引發如記憶體溢位、CPU使用率過高甚至伺服器崩潰等問題。執行緒池透過設定核心執行緒數(corePoolSize)與最大執行緒數(maximumPoolSize),能夠動態調整併發執行的任務數,確保系統的穩定性和資源的有效利用。比如,當核心執行緒已滿負荷工作時,非核心執行緒會在任務佇列飽和後才開始建立,且一旦超過最大執行緒數,執行緒池會根據配置的拒絕策略對新提交的任務進行合理處理。

便於統一管理和維護執行緒 執行緒池提供了統一的執行緒管理和異常處理機制,使得程式設計師無需關注每個執行緒的具體建立和銷燬過程,簡化了程式碼邏輯。執行緒池還可以為執行緒設定優先順序、命名以及自定義執行緒工廠等特性,進一步增強了執行緒管理的靈活性和可定製性。此外,執行緒池還支援任務完成後的回撥函式,如beforeExecute()afterExecute()方法,用於執行特定的前後置操作,提升了程式的整體可控性和健壯性。

透過使用執行緒池,我們可以更有效地組織併發執行的任務,降低開發難度,同時提高了系統的響應速度和資源使用效率。以下是一個簡單的示例,展示瞭如何利用執行緒池執行多個耗時任務並控制併發數量:

class MyTask implements Runnable {
private int taskId;

public MyTask(int id) {
this.taskId = id;
}

@Override
public void run() {
System.out.println("Task " + taskId + " is running in thread: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

// 使用執行緒池執行多個任務
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executor.execute(new MyTask(i));
}
executor.shutdown();

在這個例子中,即使有10個任務需要執行,但由於執行緒池大小被限制為5,所以最多隻有5個任務會同時被執行,有效避免了併發數量過大帶來的潛在問題。同時,當所有任務完成後,呼叫shutdown()方法優雅關閉執行緒池,確保資源得到釋放。

執行緒池介面與實現


在Java中,執行緒池的實現基於java.util.concurrent包中的Executor介面及其擴充套件介面。其中,ThreadPoolExecutor作為核心實現類,提供了豐富的配置選項和靈活的任務排程機制。

Java Executor 介面

Executor介面定義了一個統一的方法execute(Runnable command),用於執行提交給它的Runnable任務,簡化了執行緒建立和管理的過程。透過實現這個介面,可以建立具有不同策略的執行緒池,例如:

Executor executor = Executors.newFixedThreadPool(10); // 建立固定大小執行緒池
executor.execute(new Runnable() {
@Override
public void run() {
// 業務邏輯程式碼
}
});

ThreadPoolExecutor 類

構造方法詳解 ThreadPoolExecutor提供了一系列建構函式,允許開發者自定義執行緒池的核心引數。主要包含以下五個基本引數:

  • corePoolSize: 核心執行緒數,即使沒有任務處理時也會保留線上程池中的執行緒數量。
  • maximumPoolSize: 執行緒池最大容量,當工作佇列滿載且仍有新任務到來時,執行緒池將嘗試增加到此值。
  • keepAliveTime: 非核心執行緒空閒超時時長,在指定時間內無新任務分配給非核心執行緒,則會銷燬這些執行緒。
  • unit: keepAliveTime的時間單位,如秒、毫秒等。
  • workQueue: 用於儲存待執行任務的阻塞佇列,型別可選為LinkedBlockingQueue、ArrayBlockingQueue、SynchronousQueue或DelayQueue等。

例如:

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 8, 60L, TimeUnit.SECONDS, queue);

此處建立了一個初始核心執行緒數為4、最大執行緒數為8的執行緒池,閒置非核心執行緒超過60秒會被回收,並使用鏈式阻塞佇列來存放任務。

此外,還有兩個額外的可選引數:

  • threadFactory: 定義執行緒工廠,用於批次建立執行緒並設定其屬性(如守護執行緒、優先順序等)。預設採用DefaultThreadFactory,可以根據需求自定義實現。
  • handler: 拒絕策略,當執行緒池及任務佇列飽和時,無法接收新的任務時所採取的動作,預設為AbortPolicy,即丟擲RejectedExecutionException異常。還可以選擇DiscardPolicy(直接丟棄任務)、DiscardOldestPolicy(丟棄最早進入佇列的任務以騰出空間)或CallerRunsPolicy(由提交任務的執行緒自行執行該任務)。

執行緒池執行流程

一個典型的執行緒池例項化示例是建立一個僅有一個核心執行緒的執行緒池,確保所有任務按順序執行,不建立非核心執行緒:

ExecutorService executor = new ThreadPoolExecutor(
1, // corePoolSize
1, // maximumPoolSize
0L, // keepAliveTime
TimeUnit.MILLISECONDS, // unit
new LinkedBlockingQueue<>() // workQueue
);

此執行緒池不會因為執行緒數量不足而建立額外的非核心執行緒,所有提交的任務都會按照FIFO原則新增到佇列中等待唯一的核心執行緒執行。

總之,Java中的執行緒池介面與其實現(尤其是ThreadPoolExecutor)為開發者提供了強大的併發程式設計工具,允許我們根據應用場景靈活調整執行緒資源的分配和管理策略,從而有效地提升程式效能和系統穩定性。透過深入理解其內部構造原理和執行機制,我們可以更好地設計和最佳化多執行緒應用。

執行緒池狀態


在Java的多執行緒程式設計中,執行緒池的狀態與生命週期管理是其核心功能之一。ThreadPoolExecutor類透過維護一個volatile int型別的變數runState來表示執行緒池的狀態,該狀態包括RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED五個階段。

  • RUNNING:執行緒池建立後預設處於此狀態,能夠接受新任務並處理阻塞佇列中的任務。
  • SHUTDOWN:呼叫shutdown()方法後進入此狀態,不再接受新的提交任務,但會繼續處理已加入佇列的任務直至執行完畢。
ExecutorService executor = Executors.newFixedThreadPool(5);
// ... 執行一系列任務
executor.shutdown();

  • STOP:呼叫shutdownNow()方法後變為STOP狀態,不僅不接收新任務,還會嘗試中斷正在執行的任務,並且不會處理尚未開始執行的任務。
executor.shutdownNow(); // 立即停止所有正在執行的任務並拒絕後續任務

  • TIDYING:當所有的任務都已經終止並且workerCount(活動工作執行緒數)為0時,執行緒池將轉換到TIDYING狀態,並觸發terminated()鉤子方法。
  • TERMINATEDterminated()方法執行完畢後,執行緒池最終進入TERMINATED狀態,表明執行緒池已經徹底關閉,無法再進行任何操作。

執行緒池狀態的變化過程遵循嚴格的條件判斷和轉換邏輯,例如在任務執行流程中,新增任務或銷燬執行緒時都會檢查當前的runState。此外,執行緒池還透過ctl變數合併了workerCount(工作執行緒數量)和runState的資訊,以原子方式更新執行緒池的整體狀態。

下面是一個簡單的示例,演示瞭如何監控執行緒池的狀態變化:

public class ThreadPoolStatusMonitor {
private final ThreadPoolExecutor executor;

public ThreadPoolStatusMonitor(ThreadPoolExecutor executor) {
this.executor = executor;
executor.addThreadFactory(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("MonitoringThread");
return thread;
}
});

ScheduledExecutorService monitor = Executors.newSingleThreadScheduledExecutor();
monitor.scheduleAtFixedRate(() -> {
System.out.println("Current pool status: " + getStateString(executor));
if (executor.isTerminated()) {
monitor.shutdown();
}
}, 1, 1, TimeUnit.SECONDS);
}

private String getStateString(ThreadPoolExecutor executor) {
switch (executor.getRunState()) {
case RUNNING:
return "RUNNING";
case SHUTDOWN:
return "SHUTDOWN";
case STOP:
return "STOP";
case TIDYING:
return "TIDYING";
case TERMINATED:
return "TERMINATED";
default:
return "UNKNOWN";
}
}
}

// 使用示例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
new ThreadPoolStatusMonitor(executor);

// 新增一些任務...
executor.execute(() -> { /* 業務邏輯 */ });

// 後續呼叫 shutdown 或 shutdownNow 方法
executor.shutdown();

透過上述程式碼片段可以看到,我們建立了一個執行緒池狀態監測器,定期列印執行緒池狀態,並在檢測到執行緒池終止後自動停止監測執行緒。這樣可以直觀地觀察到執行緒池從建立到最終關閉整個生命週期內的狀態變化情況。

執行緒池處理流程


執行緒池任務處理流程是ThreadPoolExecutor類的核心功能,其主要透過execute(Runnable command)方法實現。下面我們將深入剖析該方法內部的任務排程邏輯。

建立核心執行緒執行任務(corePoolSize) 當呼叫execute()方法提交任務時,首先檢查當前活躍執行緒數是否小於核心執行緒數(corePoolSize)。如果是,則直接建立新的核心執行緒來執行這個新任務。核心執行緒在沒有任務可執行時不會被銷燬,除非設定了允許核心執行緒超時的選項。

if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true)) // 核心執行緒,並嘗試新增到工作佇列中
return;
}

將任務新增到任務佇列(workQueue) 如果當前活躍執行緒數不小於核心執行緒數,接下來會嘗試將任務放入阻塞佇列(workQueue)中等待空閒的核心執行緒去執行。在這個階段,會進行兩次執行緒池狀態檢查:一次是在入隊前,另一次是在成功入隊後。這是因為在多執行緒環境下,執行緒池的狀態可能會隨時發生變化,因此需要二次檢查以確保任務能夠在正確狀態下被執行。

if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command)) // 檢查狀態並移除任務
reject(command); // 執行拒絕策略
else if (workerCountOf(recheck) == 0) // 如果此時沒有活動執行緒則建立非核心執行緒
addWorker(null, false);
}

建立非核心執行緒執行任務(maximumPoolSize) 若任務無法放入任務佇列,這可能是因為佇列已滿或執行緒池配置不允許放入更多工。在這種情況下,執行緒池試圖建立非核心執行緒來執行任務,但僅在匯流排程數未達到最大值(maximumPoolSize)的情況下才建立。

else if (!addWorker(command, false)) // 建立非核心執行緒執行任務
reject(command); // 若建立失敗則執行拒絕策略

拒絕策略 當執行緒池無法再接受新任務時(例如超過最大執行緒數且任務佇列已滿),則觸發拒絕策略。Java提供了四種預定義的拒絕策略:

  • AbortPolicy:預設策略,丟擲RejectedExecutionException異常。
  • DiscardPolicy:默默地丟棄任務,不做任何處理。
  • DiscardOldestPolicy:丟棄佇列中最舊的任務(即最先加入佇列的任務),然後重新嘗試執行新任務。
  • CallerRunsPolicy:由呼叫執行緒執行被拒絕的任務。

總結整個處理流程

  1. 當執行緒數量不足corePoolSize時,優先建立核心執行緒執行任務。
  2. 執行緒數量滿足corePoolSize時,將任務加入workQueue等待執行。
  3. workQueue已滿且執行緒數量未達maximumPoolSize時,建立非核心執行緒執行任務。
  4. 若所有條件均無法接納新任務,則根據設定的拒絕策略處理被拒絕的任務。

以下是一個簡化的示例程式碼,展示如何使用執行緒池執行任務:

public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心執行緒數
5, // 最大執行緒數
60L, // 空閒執行緒存活時間
TimeUnit.SECONDS,
new LinkedBlockingQueue<>() // 使用無界鏈式阻塞佇列
);

for (int i = 0; i < 10; i++) {
Runnable task = () -> System.out.println("Task " + Thread.currentThread().getName() + " is running");
executor.execute(task);
}

executor.shutdown(); // 提交完所有任務後關閉執行緒池
}

這段程式碼建立了一個執行緒池,並提交了10個任務,根據上述任務處理流程,執行緒池會按照合適的方式安排這些任務的執行。

執行緒複用機制原理


執行緒複用機制是Java執行緒池實現高效併發處理的核心技術之一,其主要透過ThreadPoolExecutor類中的Worker工作執行緒類來完成。Worker不僅實現了Runnable介面,還是一個封裝了執行緒和任務佇列互動的實體。

在建立執行緒池時,首先會建立一定數量的核心執行緒(corePoolSize),這些執行緒會一直存活線上程池中,即使沒有任務執行,除非設定了允許核心執行緒超時策略。當有新任務提交到執行緒池時,首先嚐試將任務分配給這些核心執行緒。如果所有核心執行緒都在忙碌,且任務佇列非空,則新提交的任務會被放入阻塞佇列等待執行。

Worker類的建構函式初始化了一個與之關聯的Thread物件,並將其自身作為該執行緒的任務,即當呼叫t.start()啟動這個執行緒時,實際執行的是Worker.run()方法。在run()方法中,Worker會不斷地從阻塞佇列中獲取新的任務來執行,這個過程如下:

final void runWorker(Worker w) {
// 獲取當前執行的執行緒以及初始任務
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;

// 清除firstTask並解鎖,以便執行後續任務
w.firstTask = null;
w.unlock(); // allow interrupts

try {
// 無限迴圈,直到執行緒池停止或worker退出
while (task != null || (task = getTask()) != null) {
// 加鎖並檢查執行緒池狀態,若已關閉則中斷執行緒
w.lock();
// ... 狀態判斷及中斷操作

try {
// 執行前置鉤子方法
beforeExecute(wt, task);

// 執行任務
task.run();

// 執行後置鉤子方法
afterExecute(task, null);
} catch (...) { ... }

// 更新已完成任務計數並解鎖
task = null;
w.completedTasks++;
w.unlock();
}
} finally {
// 工作執行緒退出時進行資源清理
processWorkerExit(w, completedAbruptly);
}
}

getTask()方法負責從阻塞佇列中取出下一個待執行的任務。根據執行緒池配置,核心執行緒會使用workQueue.take()方法阻塞等待新任務;而非核心執行緒在keepAliveTime內未獲得新任務時,會呼叫workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)方法嘗試獲取,超時後執行緒可能會被銷燬。

下面是一個簡化的示例程式碼片段,展示瞭如何利用執行緒池執行任務並實現執行緒複用:

public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心執行緒數
10, // 最大執行緒數
60L, // 空閒執行緒存活時間
TimeUnit.SECONDS,
new LinkedBlockingQueue<>() // 阻塞佇列
);

for (int i = 0; i < 20; i++) {
final int taskId = i;
executor.execute(new Runnable() {
@Override
public void run() {
System.out.println("Task " + taskId + " running in thread: " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模擬耗時任務
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
}

executor.shutdown(); // 提交完所有任務後關閉執行緒池
}

在這個例子中,執行緒池會根據需要建立並複用執行緒,每個任務都由執行緒池中的一個執行緒執行,任務完成後執行緒並不會立即銷燬,而是繼續從佇列中獲取下一個任務執行,從而達到複用的目的。同時,執行緒池內部管理確保了執行緒生命週期的合理控制,避免了頻繁建立和銷燬執行緒帶來的開銷。

總結


Java執行緒池原理的核心在於ThreadPoolExecutor類的實現,它透過合理管理執行緒生命週期、任務佇列以及執行緒池狀態來高效地執行併發任務。執行緒池利用核心執行緒和非核心執行緒複用機制,有效降低了系統資源消耗,控制了併發數量,並簡化了執行緒的統一管理和異常處理。

首先,執行緒池透過構造方法設定引數如corePoolSize(核心執行緒數)、maximumPoolSize(最大執行緒數)和keepAliveTime(空閒執行緒存活時間),以及選擇合適的阻塞佇列workQueue(例如LinkedBlockingQueue、ArrayBlockingQueue或SynchronousQueue等)。透過這些引數,開發者可以根據應用需求靈活調整執行緒池的行為特性。

在處理任務時,execute()方法作為入口點,根據當前執行緒池狀態和執行緒數決定如何排程任務:優先使用核心執行緒執行任務,當核心執行緒已滿載時將任務放入阻塞佇列;若阻塞佇列也已滿且執行緒總數未達到最大值,則建立非核心執行緒執行新任務;若超過最大執行緒數則採用預定義的拒絕策略(AbortPolicy、DiscardPolicy、DiscardOldestPolicy或CallerRunsPolicy)。

執行緒複用的關鍵在於Worker類的設計。每個Worker物件封裝了一個Thread例項並實現了Runnable介面,其run()方法會持續從工作佇列中獲取任務並執行,實現了執行緒在完成一個任務後能夠立即投入下一個任務的執行,從而避免了頻繁建立和銷燬執行緒帶來的開銷。

此外,執行緒池的狀態機設計至關重要,包含RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED五個狀態,分別對應不同的行為模式。例如,呼叫shutdown()方法後,執行緒池進入SHUTDOWN狀態,不再接受新提交的任務但繼續執行已在佇列中的任務,直至所有任務執行完畢並透過terminated()方法轉換為TERMINATED狀態。

總之,在多執行緒程式設計中,Java執行緒池為我們提供了一種強大而靈活的工具,透過合理配置和管理執行緒池,不僅能有效提升程式效能,還能確保系統的穩定性和資源的有效利用。實際開發中,應當根據業務需求定製化執行緒池引數,並充分理解執行緒池的工作原理與任務排程邏輯,以便於編寫出高併發、低資源佔用的健壯程式碼。

示例程式碼:

// 建立固定大小的執行緒池,核心執行緒數等於最大執行緒數,無界任務佇列
ExecutorService executor = Executors.newFixedThreadPool(5);

for (int i = 0; i < 10; i++) {
final int taskId = i;
Runnable task = () -> System.out.println("Task " + taskId + " executed by " + Thread.currentThread().getName());
executor.execute(task);
}

executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS); // 等待所有任務完成

// 示例展示了執行緒池如何接收多個任務並分配給執行緒執行,最終關閉執行緒池並確保所有任務都已完成。

這段程式碼展示瞭如何建立一個固定大小的執行緒池,並提交多個任務到執行緒池進行非同步執行。透過呼叫awaitTermination()方法,主程式可以等待所有任務完成後再結束執行,確保了任務的正確完成和執行緒池的有序關閉。

本文使用 markdown.com.cn 排版

相關文章