在軟體開發不斷髮展的世界中,有效管理併發任務的能力至關重要。傳統的執行緒方法可能變得繁瑣且容易出錯,特別是在處理大量非同步操作時。這時,ExecutorService
登場了:它是 Java 併發框架中一個強大的抽象,旨在簡化和最佳化非同步任務執行。
本文我們將深入探討 ExecutorService
其核心功能,探索各種執行緒池配置,以解決 Java 應用程式中的現實世界併發挑戰。透過閱讀、實踐本文內容,你將掌握以下能力;
- 簡化非同步程式設計: 抽象執行緒管理允許專注於任務的邏輯,而不是執行緒建立和生命週期的細節。
- 提高可擴充套件性: 輕鬆管理執行緒池,使應用程式能夠高效地處理不同的工作負載。
- 增強可維護性: 透過集中執行緒管理實現更乾淨、更易讀和可維護的應用程式。
ExecutorService 概覽
傳統上,執行緒被用來實現併發。每個執行緒就像處理器上的一個單獨核心,一次專注於一個任務。然而,直接管理執行緒可能是一個 雜技
行為。這種做法需要擔心建立執行緒、處理它們的生命週期(啟動、停止)以及當多個執行緒訪問共享資源(如資料庫或檔案系統)時可能出現的同步問題。
這就是 ExecutorService 可靠的地方。它作為管理 非同步任務 的高階抽象。非同步任務本質上是可以獨立提交和執行的工作,而不必阻塞主執行執行緒。ExecutorService 負責執行緒池管理,讓開發者專注於任務的邏輯,而不是執行緒的複雜性。
使用 ExecutorService 與原始執行緒管理相比的好處:
- 簡化的程式碼: 編寫的是任務的功能程式碼(例如,處理影像、下載檔案),而不是執行緒管理(建立和管理工作執行緒)。這導致程式碼更乾淨、更易於維護。
-
提高的可擴充套件性:
ExecutorService
管理一個執行緒池。如果一個執行緒忙於處理影像,另一個可以下載檔案,確保程式能夠高效地處理不同的工作負載。 -
增強的錯誤處理:
ExecutorService
提供了處理任務執行期間可能發生的錯誤的方法。開發者不必編寫單獨的程式碼來捕獲和處理個別執行緒丟擲的異常。 -
資源管理:
ExecutorService
控制執行緒池中的執行緒數量,防止建立太多執行緒,可能會壓垮系統的資源。這就像有一個定義好的工作執行緒數量,以避免用太多工超載處理器。
基礎功能
現在我們已經理解了 ExecutorService 的魔力,讓我們看看如何將其付諸實踐。以下是如何建立一個ExecutorService
例項並提交任務進行非同步執行的方法:
建立 ExecutorService
Java 的 Executors
類提供了各種工廠方法來建立不同型別的 ExecutorService
例項。這裡是一個常見的例子:
ExecutorService executorService = Executors.newFixedThreadPool(5);
這段程式碼建立了一個大小為 5 的固定執行緒池的 ExecutorService
。這意味著 ExecutorService
將管理一個 5 個執行緒的池來執行您的任務。您可以根據您的需求選擇其他配置,如newSingleThreadExecutor
(一個執行緒)或newCachedThreadPool
(動態調整執行緒池大小)。
提交任務
有兩種主要方式向 ExecutorService 提交任務:
-
submit(Callable<T> task)
: 這個方法接受一個Callable
物件作為輸入。Callable
介面擴充套件了Runnable
,但允許開發者從任務執行中返回結果。當呼叫submit
時,ExecutorService
安排任務執行並返回一個Future
物件。 -
execute(Runnable task)
: 這個方法接受一個Runnable
物件作為輸入。Runnable
介面定義了一個要非同步執行的單個方法run()
。與submit
不同,execute
不返回結果。
Future 的力量:管理任務執行
當我們使用submit(Callable<T> task)
時,ExecutorService
返回一個Future
物件。這個Future
物件作為任務最終結果的佔位符。它提供了幾種管理任務執行的方法:
-
get(): 這個方法阻塞呼叫執行緒,直到任務完成執行,然後返回
Callable
中的call()
方法產生的結果。 -
isDone(): 這個方法檢查任務是否已完成執行。如果任務已完成,則返回
true
;否則返回false
。 -
cancel(boolean mayInterruptIfRunning): 這個方法嘗試取消任務執行。
mayInterruptIfRunning
引數指定是否應該中斷當前執行的執行緒。
執行緒池機制
ExecutorService
的核心是一個強大的概念——執行緒池。它就像一群等待被工頭(ExecutorService)分配任務的工人(執行緒)。理解執行緒池對於有效利用 ExecutorService
至關重要。
執行緒池在行動
工頭(ExecutorService)有一群具有特定技能(任務型別)的工人(執行緒)。當建築任務(提交)到達時,工頭將它們分配給池中的可用工人。這確保了任務的有效執行,而不需要為每個單獨的任務建立新的工人。
選擇正確的配置
Executors
類提供了各種工廠方法,用於建立具有不同執行緒池配置的 ExecutorService
例項:
-
newFixedThreadPool(int nThreads)
: 這個方法建立了一個大小為nThreads
的固定執行緒池的 ExecutorService。這適用於工作負載可預測的場景。固定池大小確保了一致的併發級別,但如果工作負載超過了可用執行緒,任務可能會排隊等待可用的工人。 -
newSingleThreadExecutor()
: 這個方法建立了一個池中只有一個執行緒的 ExecutorService。這適用於需要嚴格順序執行或彼此依賴的任務。然而,它限制了併發性,並且可能不適用於處理多個獨立任務。 -
newCachedThreadPool()
: 這個方法建立了一個動態調整大小的執行緒池的 ExecutorService。池大小可以根據需要增長以處理傳入的任務。然而,這種靈活性帶來了潛在的缺點:
平衡效能和資源
執行緒池的大小和配置顯著影響應用程式的效能和資源利用。以下是如何找到平衡的方法:
- 執行緒池大小: 更大的執行緒池允許更多的併發任務執行,但它也消耗更多的資源。選擇一個與平均工作負載相符的大小,以避免資源耗盡或未充分利用。
-
排隊行為: 當執行緒池已滿且沒有工人可用時,任務可能會排隊等待稍後執行。
Executors
類不直接控制排隊行為。然而,一些底層實現可能使用有界佇列(如果佇列已滿,則拒絕任務)或無界佇列(即使佇列已滿,任務也會繼續新增,可能導致OutOfMemoryError
異常)。
ExecutorService 高階功能
現在我們已經探討了 ExecutorService
的核心功能,讓我們深入瞭解一些高階主題,以獲得全面、深刻的理解:
優雅關閉
ExecutorService
不應該被突然遺棄。以下是兩個關鍵方法,用於優雅地關閉它:
-
shutdown()
: 這個方法向 ExecutorService 發出訊號,表示不應再提交新任務。佇列中的任務或當前正在執行的任務將被允許完成,然後 ExecutorService 才會終止。這就像告知工頭ExecutorService
停止接受新的建築工程任務,但允許正在進行的專案完成。 -
shutdownNow()
: 這個方法嘗試停止所有當前正在執行的任務,並防止任何新任務被提交。這就像工頭緊急停止所有建築活動。然而,要小心——突然停止任務可能導致工作不完整或資料不一致。
拒絕策略
當我們向一個已滿的執行緒池的 ExecutorService
提交任務時,如果執行緒池無法接受新的任務,這些任務會被提交到執行緒池的阻塞佇列中。如果阻塞佇列也已滿,就會觸發一個名為 RejectedExecutionHandler
的機制。預設情況下,ThreadPoolExecutor
使用的拒絕策略是 AbortPolicy
,即丟擲 RejectedExecutionException
異常,導致任務被拒絕。
為了處理被拒絕的任務,我們可以選擇不同的策略:
-
AbortPolicy(預設策略):
- 直接丟擲
RejectedExecutionException
異常,阻止任務被提交。 - 適用於需要立即發現問題並採取措施的場景。
- 直接丟擲
-
CallerRunsPolicy:
- 呼叫者執行緒會執行被拒絕的任務,這樣可以降低新的任務被提交的速度。
- 適用於希望繼續執行被拒絕任務但不需要嚴格的執行順序的情況。
-
DiscardPolicy:
- 直接丟棄被拒絕的任務,不丟擲異常。
- 適用於可以接受部分任務被丟棄的情況,但這種策略可能導致任務丟失而不易察覺。
-
DiscardOldestPolicy:
- 丟棄阻塞佇列中最舊的任務,然後重新嘗試提交當前任務。
- 適用於更關注最新任務而願意捨棄舊任務的場景。
-
自定義處理策略:
- 透過實現
RejectedExecutionHandler
介面,開發者可以定義自己的拒絕處理邏輯。例如,可以記錄日誌、報警,甚至將任務重新加入佇列。
- 透過實現
選擇適合的拒絕策略,能幫助你更好地控制執行緒池在高負載情況下的行為,確保系統的穩定性。
多工協調
ExecutorService
提供了超出簡單任務提交的功能:
-
invokeAll(Collection<? extends Callable<T>> tasks)
: 這個方法允許提交一系列Callable
任務,並返回包含結果的List<Future>
。它阻塞呼叫執行緒直到所有任務完成。這適用於等待一組任務的完成並檢索它們各自的結果。 -
invokeAny(Collection<? extends Callable<T>> tasks)
: 這個方法提交一系列Callable
任務,但只等待第一個完成。它返回已完成任務的結果,如果所有任務都失敗則丟擲異常。這在您只需要一個組中任何一個任務的結果時非常有用,無論哪個任務首先完成。
ExecutorService 實踐
ExecutorService
在許多需要非同步處理以提高效能和響應性的場景中表現出色。讓我們透過一些程式碼示例來探索一些常見用例:
網路請求
當需要從多個 API 併發地獲取資料以提升 Web 應用程式的感知效能時,ExecutorService
可以發揮重要作用。透過它,我們可以高效地管理執行緒池,提交多個並行任務,從而在最短的時間內獲取所有 API 的響應。這種方式不僅提升了資料獲取速度,還減少了單個 API 請求的等待時間,從而顯著改善使用者體驗。
以下是一個使用 ExecutorService
併發地從多個 API 獲取資料的示例:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.List;
import java.util.ArrayList;
public class ConcurrentApiRequests {
public static void main(String[] args) {
// 建立一個固定大小的執行緒池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 假設我們有多個API的URL列表
List<String> apiUrls = List.of(
"https://funtester.com/posts/1",
"https://funtester.com/posts/2",
"https://funtester.com/posts/3"
);
// 儲存每個任務的Future物件
List<Future<String>> futures = new ArrayList<>();
for (String url : apiUrls) {
Callable<String> task = () -> {
// 模擬傳送HTTP請求(使用 HttpClient 或 OkHttp 等工具)
// 這裡為了簡化,直接返回URL
return fetchDataFromApi(url);
};
// 提交任務給執行緒池
Future<String> future = executor.submit(task);
futures.add(future);
}
// 處理所有的Future物件,獲取結果
for (Future<String> future : futures) {
try {
// 獲取任務執行的結果
String result = future.get();
System.out.println("API Response: " + result);
} catch (Exception e) {
e.printStackTrace();
}
}
// 關閉執行緒池
executor.shutdown();
try {
// 等待所有任務完成
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // 強制關閉
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
// 模擬API資料獲取
private static String fetchDataFromApi(String url) throws Exception {
// 這裡可以實際呼叫HttpClient或其他HTTP庫傳送請求並獲取資料
// 為了簡化,直接返回URL作為結果
Thread.sleep(1000); // 模擬網路延遲
return "Data from " + url;
}
}
關鍵步驟說明:
-
執行緒池建立: 使用
Executors.newFixedThreadPool(3)
建立一個固定大小為 3 的執行緒池,允許最多 3 個任務併發執行。 -
提交任務: 將每個 API 請求封裝為一個
Callable
任務,並提交給ExecutorService
,返回一個Future
物件。Future
用於非同步獲取任務的執行結果。 -
處理任務結果: 透過遍歷
Future
列表,呼叫future.get()
獲取每個任務的結果。此操作是阻塞的,但因為任務是併發執行的,整體效能大大提升。 -
執行緒池關閉: 呼叫
executor.shutdown()
關閉執行緒池,並使用awaitTermination
等待所有任務完成。如果等待超時,則呼叫shutdownNow
強制關閉。
透過 ExecutorService
併發地從多個 API 獲取資料,可以顯著提升 Web 應用程式的感知效能,讓使用者感受到更快的響應速度和更流暢的互動體驗。在實際應用中,根據 API 的特性和系統資源合理調整執行緒池配置,是獲得最佳效能的關鍵。
影像處理
在需要對一批上傳的影像進行後臺處理(如調整影像大小)時,ExecutorService
是一個非常有效的工具。它可以非同步處理這些任務,而不會阻塞主執行緒,從而保持應用程式的響應性。
以下是一個使用 ExecutorService
來非同步調整一批影像大小的示例:
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.imageio.ImageIO;
public class ImageResizer {
public static void main(String[] args) {
// 建立一個固定大小的執行緒池
ExecutorService executor = Executors.newFixedThreadPool(4);
// 模擬上傳的影像檔案列表
List<File> imageFiles = List.of(
new File("path/to/image1.jpg"),
new File("path/to/image2.jpg"),
new File("path/to/image3.jpg")
);
// 儲存每個任務的Future物件
List<Future<File>> futures = new ArrayList<>();
for (File imageFile : imageFiles) {
Callable<File> task = () -> resizeImage(imageFile, 200, 200); // 將影像調整為200x200的大小
// 提交任務給執行緒池
Future<File> future = executor.submit(task);
futures.add(future);
}
// 影像大小調整方法
private static File resizeImage(File inputFile, int width, int height) throws IOException {
// 讀取原始影像
BufferedImage originalImage = ImageIO.read(inputFile);
// 建立一個縮放後的影像
Image scaledImage = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
// 在新的BufferedImage中繪製縮放後的影像
Graphics2D g2d = resizedImage.createGraphics();
g2d.drawImage(scaledImage, 0, 0, null);
g2d.dispose();
// 儲存調整大小後的影像
File outputFile = new File("resized_" + inputFile.getName());
ImageIO.write(resizedImage, "jpg", outputFile);
return outputFile;
}
}
程式碼步驟說明:
-
執行緒池建立: 使用
Executors.newFixedThreadPool(4)
建立一個固定大小為 4 的執行緒池,允許最多 4 個影像處理任務併發執行。 -
提交任務: 將每個影像的大小調整操作封裝為一個
Callable<File>
任務,並提交給ExecutorService
。每個任務返回一個Future<File>
,用於非同步獲取處理結果。 -
影像大小調整: 在
resizeImage
方法中,透過Image.getScaledInstance
方法調整影像大小,並使用Graphics2D
將縮放後的影像繪製到新的BufferedImage
上,然後將其儲存為新的檔案。 -
處理任務結果: 透過遍歷
Future
列表,呼叫future.get()
獲取每個任務的結果。此操作阻塞當前執行緒直到任務完成,但由於任務是併發執行的,整個過程依然很高效。 -
執行緒池關閉: 呼叫
executor.shutdown()
關閉執行緒池,確保所有任務完成後再結束應用。
這種方法適用於 Web 應用、桌面應用或伺服器後臺任務,例如使用者上傳多張圖片時,應用可以迅速響應使用者上傳操作,而在後臺調整圖片大小,以便稍後用於展示或儲存。
後臺任務
在應用程式中,某些任務可能需要在後臺執行,例如傳送電子郵件、記錄資料、處理檔案等。這些任務通常需要一定的時間完成,而如果在主執行緒中執行這些任務,可能會導致應用程式的 UI 變得不響應。為了保持 UI 的流暢性和使用者體驗,使用 ExecutorService
來非同步處理這些後臺任務是非常有效的。
下面是一個簡單的示例,展示瞭如何使用 ExecutorService
來非同步傳送電子郵件和記錄資料,而不阻塞主執行緒:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class BackgroundTaskExample {
public static void main(String[] args) {
// 建立一個單執行緒的ExecutorService,用於處理後臺任務
ExecutorService executor = Executors.newSingleThreadExecutor();
// 模擬UI操作,例如使用者點選按鈕後觸發的任務
System.out.println("UI is responsive. Performing background tasks...");
// 非同步傳送電子郵件
executor.submit(() -> sendEmail("user@example.com", "Welcome", "Thank you for signing up!"));
// 非同步記錄資料
executor.submit(() -> logData("User signed up with email user@example.com"));
// 主執行緒繼續執行其他操作
System.out.println("UI can continue to interact with the user...");
// 關閉ExecutorService
executor.shutdown();
}
// 模擬傳送電子郵件的方法
private static void sendEmail(String recipient, String subject, String body) {
try {
// 模擬傳送電子郵件的延遲
Thread.sleep(2000);
System.out.println("Email sent to " + recipient + " with subject: " + subject);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Email sending was interrupted");
}
}
// 模擬記錄資料的方法
private static void logData(String data) {
try {
// 模擬記錄資料的延遲
Thread.sleep(1000);
System.out.println("Data logged: " + data);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Data logging was interrupted");
}
}
}
程式碼說明:
-
執行緒池建立: 使用
Executors.newSingleThreadExecutor()
建立一個單執行緒的ExecutorService
。這種型別的執行緒池適合處理多個任務但確保任務按順序執行。 -
非同步任務提交: 使用
executor.submit()
提交任務。這些任務在後臺非同步執行,主執行緒無需等待任務完成即可繼續處理其他操作。 -
任務實現: 任務的實現可以是任何需要耗時操作的程式碼,例如傳送電子郵件或記錄資料。為了模擬這些操作的延遲,使用了
Thread.sleep()
。 - 主執行緒的執行: 在任務執行的同時,主執行緒依然保持對 UI 的控制權,可以繼續響應使用者操作。透過這種方式,應用程式的響應性得以保持。
-
執行緒池關閉: 在所有任務提交完畢後,呼叫
executor.shutdown()
關閉ExecutorService
。這種關閉方式允許已經提交的任務繼續執行,而不會接受新任務。
實際應用場景:
- 傳送通知: 當使用者完成某些操作後,應用程式可能需要傳送確認郵件或簡訊。這些操作可以非同步進行,不阻礙使用者的後續操作。
- 資料處理: 使用者提交資料後,應用可以立即響應使用者,而將資料處理的任務(如儲存到資料庫、生成報告等)交給後臺執行緒執行。
- 日誌記錄: 應用程式可以在後臺記錄重要操作日誌,而不會影響前臺的使用者互動。
透過 ExecutorService
,我們可以有效地將這些後臺任務與主執行緒分離,實現應用程式的高效執行和良好的使用者體驗。
經驗分享
最佳實踐
選擇正確的 ExecutorService
配置對最佳化效能和資源至關重要。以下是一些最佳實踐:
- 分析您的工作負載: 瞭解任務的性質(如 CPU 繫結或 I/O 繫結)和預期的併發任務數量至關重要。CPU 繫結任務需要接近 CPU 核心數的執行緒池,而 I/O 繫結任務可以使用更多執行緒。分析工作負載可以幫助確定適當的執行緒池大小,最佳化效能和資源使用。
- 深思熟慮地擴充套件: 初始時選擇較小的執行緒池規模,根據實際情況逐步增加執行緒數。這種做法可以避免因執行緒過多導致的資源耗盡問題。逐步擴充套件能更好地適應工作負載的變化,並確保系統穩定性。
-
考慮固定與快取: 對於可預測的工作負載,固定執行緒池(
FixedThreadPool
)提供穩定的效能。對於高度可變的工作負載,可選擇快取執行緒池(CachedThreadPool
),它會動態調整執行緒數量,但需注意無界佇列可能導致資源耗盡。 - 處理被拒絕的任務: 定義適當的拒絕策略來優雅地處理執行緒池滿的情況。可以記錄被拒絕的任務、稍後重試,或丟擲異常供應用程式處理。設定自定義拒絕處理器能提高系統的可靠性和靈活性。
避免的陷阱
-
資源洩漏: 使用
ExecutorService
後,務必呼叫shutdown()
或shutdownNow()
方法來關閉它。否則,執行緒池中的空閒執行緒和其他資源可能會持續佔用記憶體,導致資源洩漏和效能下降。確保在不再需要執行緒池時進行正確關閉,以維護系統資源的健康。 - 執行緒飢餓: 在使用快取執行緒池時,頻繁的短暫任務可能導致執行緒池不斷建立和銷燬執行緒。這種行為會消耗大量資源,並可能使長期執行的任務無法獲得足夠的 CPU 時間。為長時間執行的任務考慮使用固定執行緒池,這樣可以保持執行緒池的穩定性和任務處理的公平性。
-
未檢查的異常: 非同步任務在執行過程中可能會丟擲異常。如果不進行適當的異常處理,可能導致任務失敗並影響應用程式的穩定性。確保在提交任務時實現異常處理機制,捕獲並記錄異常,防止應用程式因未處理的異常崩潰。透過
Future
的get()
方法獲取任務結果時也要處理ExecutionException
。
監控和管理
- JMX(Java 管理擴充套件): JMX 提供了強大的工具來監控和管理執行緒池的效能。透過 JMX,可以實時檢視執行緒池的核心指標,如活動執行緒數、任務佇列大小和任務完成時間。這有助於即時瞭解執行緒池的執行狀況,並做出必要的調整,保持系統效能的穩定性。
- 自定義監控: 實施自定義監控解決方案可以更精確地跟蹤執行緒池的效能指標。透過定製監控指令碼或工具,可以獲取特定的指標資料,幫助識別系統瓶頸或資源耗盡問題。例如,可以監控任務提交速率、執行緒池使用情況和任務處理延遲等,以最佳化執行緒池配置和系統資源利用。
- 分析工具: 使用像 JProfiler 或 YourKit 這樣的分析工具,可以深入分析執行緒行為,識別潛在問題。這些工具能夠提供執行緒活動檢視、上下文切換情況和鎖競爭分析,幫助發現如執行緒飢餓、執行緒過度上下文切換等效能瓶頸。透過這些分析,能夠有效最佳化執行緒池配置,提高系統效能。
FunTester 原創精華
- 混沌工程、故障測試、Web 前端
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go
- 白盒、工具、爬蟲、UI 自動化
- 理論、感悟、影片