ExecutorService併發功能教程

banq發表於2024-06-16

在不斷髮展的軟體開發世界中,高效管理併發任務的能力至關重要。傳統的執行緒方法可能變得繁瑣且容易出錯,尤其是在處理大量非同步操作時。ExecutorService 就是其中的代表:Java 併發框架中的一個強大的抽象,旨在簡化和精簡非同步任務的執行。

本指南是您掌握 ExecutorService 的路線圖。我們將深入研究其核心功能,探索各種執行緒池配置,併為您提供解決 Java 應用程式中實際併發挑戰的知識。在此過程中,您將發現 ExecutorService 如何幫助您:

  • 簡化非同步程式設計:抽象執行緒管理使您能夠專注於任務的邏輯,而不是執行緒建立和生命週期的複雜性。
  • 提高可擴充套件性:輕鬆管理執行緒池,使您的應用程式能夠有效地處理不同的工作負載。
  • 增強可維護性:透過集中執行緒管理,可以實現更清晰的程式碼,從而提高應用程式的可讀性和可維護性。

ExecutorService 概覽
想象一下,一個 Web 伺服器同時處理多個請求。一個使用者可能正在瀏覽產品目錄,另一個使用者正在上傳大檔案,第三個使用者正在檢視購物車。這就是Java 中[url=https://www.javacodegeeks.com/2024/03/java-concurrency-mastering-threads-thread-pools-and-executors.html]併發[/url]的本質- 程式能夠同時處理多個任務。

傳統上,執行緒用於實現併發。每個執行緒就像處理器上的單個核心,一次專注於一項任務。但是,直接管理執行緒可能是一項繁瑣的工作。您需要擔心建立執行緒、處理執行緒的生命週期(啟動、停止)以及當多個執行緒訪問資料庫或檔案系統等共享資源時潛在的同步問題。

這時,ExecutorService就可以作為您值得信賴的助手了。它充當管理非同步任務的高階抽象。非同步任務本質上是可以獨立提交和執行的作業,而不必阻塞執行的主執行緒。ExecutorService 負責執行緒池管理,讓您專注於任務的邏輯,而不是執行緒的複雜性。

讓我們分析一下與原始執行緒管理相比使用 ExecutorService 的好處:

  • 簡化程式碼:您可以為任務的功能編寫程式碼(例如,處理影像、下載檔案),而不是執行緒管理(建立和管理工作執行緒)。這樣可以生成更簡潔、更易於維護的程式碼。
  • 提高可擴充套件性: ExecutorService 管理執行緒池。如果一個執行緒忙於處理圖片,另一個執行緒可以下載檔案,確保您的應用程式可以高效處理不同的工作負載。想象一下處理器上有多個核心,每個核心同時處理不同的任務。
  • 增強的錯誤處理: ExecutorService 提供了處理任務執行期間可能發生的錯誤機制。您不必編寫單獨的程式碼來捕獲和處理各個執行緒丟擲的異常。
  • 資源管理: ExecutorService 控制池中的執行緒數,防止建立過多執行緒,以免佔用過多的系統資源。這就像定義工作執行緒的數量,以避免處理器因太多工而過載。

核心功能:提交和管理任務
現在我們瞭解了 ExecutorService 的魔力,讓我們看看如何將其付諸實踐。以下是如何建立 ExecutorService 例項並提交任務進行非同步執行:

1.建立ExecutorService:
Java 的Executors工具類提供了各種工廠方法來建立不同型別的 ExecutorService 例項。下面是一個常見的例子:

ExecutorService executorService = Executors.newFixedThreadPool(5);

此程式碼建立一個具有固定大小為 5 的執行緒池的 ExecutorService。這意味著 ExecutorService 將管理一個由 5 個執行緒組成的池來執行您的任務。您可以根據需要選擇其他配置,例如newSingleThreadExecutor(一個執行緒)或newCachedThreadPool(動態調整執行緒池大小)。

2.提交任務:
向 ExecutorService 提交任務主要有兩種方式:

  • submit(Callable<T> task):此方法Callable以物件作為輸入。該Callable介面擴充套件Runnable但允許您返回任務執行的結果。當您呼叫時submit,ExecutorService 會安排任務執行並返回一個Future物件。
  • execute(Runnable task):此方法Runnable以物件作為輸入。該Runnable介面定義一個run()包含要非同步執行的程式碼的方法。與 不同submit,execute不返回結果。

3. Callable 與 Runnable:理解差異
和Callable都是Runnable用於定義執行緒要執行的任務的介面。但是,有一個關鍵區別:

  • Callable:此介面允許您的任務返回結果。call()其中的方法Callable定義要執行的程式碼,它可以返回任何型別的物件(<T>代表返回型別)。
  • Runnable:此介面僅定義要執行的工作單元。該run()方法不返回值。Runnable當任務不需要返回結果而只需要執行某些操作時使用。

4. Future的力量:管理任務執行
當您使用 時submit(Callable<T> task),ExecutorService 將返回一個Future物件。此Future物件充當任務最終結果的佔位符。它提供了幾種用於管理任務執行的方法:

  • get():此方法阻止呼叫執行緒,直到任務執行完畢,然後返回該call()方法在任務內產生的結果Callable。
  • isDone():此方法檢查任務是否已完成執行。true如果任務完成,則返回,false否則返回。
  • cancel(boolean mayInterruptIfRunning):該方法嘗試取消任務執行。引數mayInterruptIfRunning指定是否應中斷當前正在執行的執行緒。

執行緒池機制:理解引擎
ExecutorService 的核心是一個強大的概念——執行緒池。它就像一個工人池,等待領班(ExecutorService)分配任務。瞭解執行緒池對於有效利用 ExecutorService 至關重要。

1. 執行緒池執行
想象一下一個建築工地。工頭(ExecutorService)擁有一批具有特定技能(任務型別)的工人(執行緒)。當建築任務(提交)到達時,工頭會將其分配給池中的可用工人。這可確保高效執行任務,而無需為每個任務建立新的工人。

2. 選擇正確的配置:ExecutorService 風格
該類Executors提供了各種工廠方法來建立具有不同執行緒池配置的 ExecutorService 例項:

  • newFixedThreadPool(int nThreads):此方法建立一個具有固定大小的執行緒池的 ExecutorService nThreads。這非常適合具有可預測工作負載的場景。固定池大小可確保一致的併發級別,但如果工作負載超出可用執行緒數,任務可能會排隊等待可用的工作執行緒。
  • newSingleThreadExecutor():此方法建立一個具有池中單個執行緒的 ExecutorService。這適用於需要嚴格順序執行或相互依賴的任務。但是,它限制了併發性,並且可能不適合處理多個獨立任務。
  • newCachedThreadPool():此方法建立一個具有動態調整執行緒池的 ExecutorService。池大小可以根據需要增長以處理傳入的任務。但是,這種靈活性也存在潛在的缺點:
    • 無限制增長:如果工作負載不斷增加,執行緒池可以無限增長,可能會消耗過多的系統資源。
    • 執行緒飢餓:如果由於短期任務而不斷建立新執行緒並終止新執行緒,則由於執行緒池不斷攪動,現有任務可能會缺乏資源(CPU 時間)。

3. 平衡效能和資源:執行緒池大小和佇列
執行緒池的大小和配置會顯著影響應用程式的效能和資源利用率。以下是如何取得平衡:

  • 執行緒池大小:執行緒池越大,可同時執行的任務越多,但消耗的資源也越多。選擇與平均工作負載相匹配的大小,可避免資源耗盡或利用不足。
  • 排隊行為:當執行緒池已滿且沒有可用的工作程式時,任務可能會排隊等待稍後執行。該類Executors不直接控制排隊行為。但是,一些底層實現可能會使用有界佇列(如果佇列已滿,任務會被拒絕)或無界佇列(即使佇列已滿,任務仍會不斷新增,這可能會導致 OutOfMemoryError 異常)。

4. ExecutorService 的高階功能:微調控制
現在我們已經探索了 ExecutorService 的核心功能,讓我們深入研究一些高階主題以獲得全面的理解:

1. 優雅關機:正確地說再見
ExecutorService 不應被突然放棄。以下是正常關閉它的兩種關鍵方法:

  • shutdown():此方法向 ExecutorService 發出訊號,表示不應提交任何新任務。佇列中或當前正在執行的現有任務將被允許在 ExecutorService 終止之前完成。這就像通知工頭 (ExecutorService) 停止接受新的建築工作,但允許完成正在進行的專案。
  • shutdownNow():此方法嘗試停止所有當前正在執行的任務並阻止提交任何新任務。這就像工頭緊急叫停所有施工活動一樣。但是,請謹慎 - 突然停止任務可能會導致工作不完整或資料不一致。

2. 處理被拒絕的任務:當池子已滿時
當您向執行緒池已滿的 ExecutorService 提交任務時會發生什麼?預設情況下,任務可能會被默默丟棄,從而導致應用程式出現意外行為。以下是處理被拒絕任務的一些策略:

  • 自定義拒絕處理程式:您可以使用自定義方式配置 ExecutorService,以RejectedExecutionHandler定義如何處理被拒絕的任務。您可以實現邏輯以稍後重試任務、記錄拒絕或丟擲異常以通知應用程式。
  • BlockingQueue 實現:類中的某些執行緒池實現Executors可能使用有界佇列(例如newFixedThreadPool)。如果佇列已滿且沒有可用的工作執行緒,則該submit方法將丟擲RejectedExecutionException。這允許您在程式碼中優雅地處理異常。

3. 高階技術:協調多個任務
ExecutorService 提供的功能超出了簡單的任務提交:

  • invokeAll(Collection<? extends Callable<T>> tasks):此方法允許您提交一組Callable任務並返回List<Future>包含結果的 。它會阻止呼叫執行緒,直到所有任務完成。這對於等待一組任務完成並檢索其各自的結果很有用。
  • invokeAny(Collection<? extends Callable<T>> tasks):此方法提交一組Callable任務,但只等待第一個任務完成。它返回已完成任務的結果,如果所有任務都失敗,則丟擲異常。這在只需要組中任意一個任務的結果(無論哪個任務先完成)的情況下非常有用。

實際應用:讓 ExecutorService 發揮作用

ExecutorService 在非同步處理可以增強效能和響應能力的各種場景中表現出色。讓我們透過程式碼示例探討一些常見的用例:

1.網路請求:

想象一下同時從多個 API 獲取資料以提高 Web 應用程式的感知效能。ExecutorService 可以提供幫助的方式如下:

<font>// Define a Callable task to fetch data from a single API<i>
public static class ApiFetcher implements Callable<String> {
  private final String url;
 
  public ApiFetcher(String url) {
    this.url = url;
  }
 
  @Override
  public String call() throws Exception {
   
// Simulate API call and return response<i>
    return new HttpClient().get(url);
  }
}
 
public static void main(String[] args) throws Exception {
  ExecutorService executorService = Executors.newFixedThreadPool(3);
  List<String> urls = Arrays.asList(
"...", "...", "..."); // Replace with actual URLs<i>
 
 
// Submit tasks to fetch data from each URL<i>
  List<Future<String>> futures = new ArrayList<>();
  for (String url : urls) {
    futures.add(executorService.submit(new ApiFetcher(url)));
  }
 
 
// Process results from each Future object<i>
  for (Future<String> future : futures) {
    String data = future.get();
// Blocking call to wait for task completion<i>
   
// Process the fetched data<i>
  }
 
  executorService.shutdown();
}

2.影像處理:

假設你需要在後臺調整一批上傳圖片的大小。ExecutorService 允許你非同步處理這些任務,而不會阻塞主執行緒:

<font>// Define a Runnable task to resize an image<i>
public static class ImageResizer implements Runnable {
  private final File imageFile;
  private final int targetWidth;
 
  public ImageResizer(File imageFile, int targetWidth) {
    this.imageFile = imageFile;
    this.targetWidth = targetWidth;
  }
 
  @Override
  public void run() {
    try {
     
// Implement image resizing logic using a library like ImageJ<i>
      BufferedImage resizedImage = resizeImage(imageFile, targetWidth);
     
// Save the resized image<i>
    } catch (Exception e) {
     
// Handle exceptions gracefully<i>
    }
  }
}
 
public static void main(String[] args) throws Exception {
  ExecutorService executorService = Executors.newFixedThreadPool(4);
  List<File> images = listImagesToResize();
// Implement logic to list images<i>
 
 
// Submit tasks to resize each image<i>
  for (File image : images) {
    executorService.submit(new ImageResizer(image, 200));
// Resize to 200px width<i>
  }
 
  executorService.shutdown();
}

3.後臺任務:

您的應用程式可能需要執行傳送電子郵件或記錄資料等任務,而不會影響 UI 的響應能力。ExecutorService 非常適合此類後臺任務:


<font>// Define a Runnable task for sending an email<i>
public static class EmailSender implements Runnable {
  private final String recipient;
  private final String subject;
  private final String body;
 
  public EmailSender(String recipient, String subject, String body) {
    this.recipient = recipient;
    this.subject = subject;
    this.body = body;
  }
 
  @Override
  public void run() {
    try {
     
// Implement logic to send email using a library like JavaMail<i>
      sendEmail(recipient, subject, body);
    } catch (Exception e) {
     
// Handle exceptions gracefully (e.g., retry sending)<i>
    }
  }
}
 
public static void main(String[] args) throws Exception {
  ExecutorService executorService = Executors.newFixedThreadPool(2);
 
 
// Submit tasks to send emails<i>
  executorService.submit(new EmailSender(
"user1@example.com", "Important Update", "..."));
  executorService.submit(new EmailSender(
"user2@example.com", "Order Confirmation", "..."));
 
  executorService.shutdown();
}

這只是幾個例子,其可能性是巨大的。

有效使用的最佳實踐和注意事項
選擇正確的 ExecutorService 配置對於效能和資源最佳化至關重要。以下是一些指導您的最佳實踐:

  • 分析您的工作量:瞭解您的任務的性質(CPU 密集型、I/O 密集型)以及您預期的平均併發任務數。這將幫助您確定合適的執行緒池大小。
  • 從小處著手,謹慎擴充套件:從較小的執行緒池大小開始,然後根據實際需求逐漸增加。這可以防止執行緒過多而導致資源耗盡。
  • 考慮固定執行緒池與快取執行緒池:如果您的工作負載是可預測的,則固定執行緒池可提供一致的效能。對於高度可變的工作負載,快取執行緒池可以動態調整,但要警惕無限制的增長。
  • 處理被拒絕的任務:定義自定義拒絕處理程式,以妥善處理執行緒池已滿的情況。記錄拒絕、稍後重試任務或引發異常以供應用程式處理。

要避免的陷阱:
  • 資源洩漏:使用完 ExecutorService 後,不要忘記將其關閉。否則,空閒執行緒和資源可能會洩漏,影響效能。
  • 執行緒匱乏:使用快取執行緒池時,過多的短期任務會導致不斷建立和終止執行緒。這會消耗資源並使執行時間較長的任務無法獲得 CPU 時間。考慮對執行時間較長的任務使用固定執行緒池。
  • 未檢查異常:非同步任務可能會丟擲異常。實施適當的異常處理機制,以防止這些異常被忽視並可能導致應用程式崩潰。


    監控和管理:
    • JMX: Java 管理擴充套件 (JMX) 提供工具來監控執行緒池指標,如活動執行緒、佇列大小和完成時間。
    • 自定義監控:實施自定義監控解決方案來跟蹤執行緒池效能指標並識別潛在的瓶頸或資源耗盡。
    • 分析工具:使用 JProfiler 或 YourKit 等工具來分析執行緒行為並識別潛在問題,例如執行緒飢餓或過多的上下文切換。


     

    相關文章