Java線程池詳解

来源:https://www.cnblogs.com/CarpenterLee/archive/2018/08/30/9558026.html
-Advertisement-
Play Games

構造一個線程池為什麼需要幾個參數?如果避免線程池出現OOM?`Runnable`和`Callable`的區別是什麼?本文將對這些問題一一解答,同時還將給出使用線程池的常見場景和代碼片段。`Executors`為我們提供了構造線程池的便捷方法,對於伺服器程式我們應該杜絕使用這些便捷方法,而是直接使用線... ...


構造一個線程池為什麼需要幾個參數?如果避免線程池出現OOM?RunnableCallable的區別是什麼?本文將對這些問題一一解答,同時還將給出使用線程池的常見場景和代碼片段。

基礎知識

Executors創建線程池

Java中創建線程池很簡單,只需要調用Executors中相應的便捷方法即可,比如Executors.newFixedThreadPool(int nThreads),但是便捷不僅隱藏了複雜性,也為我們埋下了潛在的隱患(OOM,線程耗盡)。

Executors創建線程池便捷方法列表:

方法名 功能
newFixedThreadPool(int nThreads) 創建固定大小的線程池
newSingleThreadExecutor() 創建只有一個線程的線程池
newCachedThreadPool() 創建一個不限線程數上限的線程池,任何提交的任務都將立即執行

小程式使用這些快捷方法沒什麼問題,對於服務端需要長期運行的程式,創建線程池應該直接使用ThreadPoolExecutor的構造方法。沒錯,上述Executors方法創建的線程池就是ThreadPoolExecutor

ThreadPoolExecutor構造方法

Executors中創建線程池的快捷方法,實際上是調用了ThreadPoolExecutor的構造方法(定時任務使用的是ScheduledThreadPoolExecutor),該類構造方法參數列表如下:

// Java線程池的完整構造函數
public ThreadPoolExecutor(
  int corePoolSize, // 線程池長期維持的線程數,即使線程處於Idle狀態,也不會回收。
  int maximumPoolSize, // 線程數的上限
  long keepAliveTime, TimeUnit unit, // 超過corePoolSize的線程的idle時長,
                                     // 超過這個時間,多餘的線程會被回收。
  BlockingQueue<Runnable> workQueue, // 任務的排隊隊列
  ThreadFactory threadFactory, // 新線程的產生方式
  RejectedExecutionHandler handler) // 拒絕策略

竟然有7個參數,很無奈,構造一個線程池確實需要這麼多參數。這些參數中,比較容易引起問題的有corePoolSize, maximumPoolSize, workQueue以及handler

  • corePoolSizemaximumPoolSize設置不當會影響效率,甚至耗盡線程;
  • workQueue設置不當容易導致OOM;
  • handler設置不當會導致提交任務時拋出異常。

正確的參數設置方式會在下文給出。

線程池的工作順序

If fewer than corePoolSize threads are running, the Executor always prefers adding a new thread rather than queuing.
If corePoolSize or more threads are running, the Executor always prefers queuing a request rather than adding a new thread.
If a request cannot be queued, a new thread is created unless this would exceed maximumPoolSize, in which case, the task will be rejected.

corePoolSize -> 任務隊列 -> maximumPoolSize -> 拒絕策略

Runnable和Callable

可以向線程池提交的任務有兩種:RunnableCallable,二者的區別如下:

  1. 方法簽名不同,void Runnable.run(), V Callable.call() throws Exception
  2. 是否允許有返回值,Callable允許有返回值
  3. 是否允許拋出異常,Callable允許拋出異常。

Callable是JDK1.5時加入的介面,作為Runnable的一種補充,允許有返回值,允許拋出異常。

三種提交任務的方式:

提交方式 是否關心返回結果
Future<T> submit(Callable<T> task)
void execute(Runnable command)
Future<?> submit(Runnable task) 否,雖然返回Future,但是其get()方法總是返回null

如何正確使用線程池

避免使用無界隊列

不要使用Executors.newXXXThreadPool()快捷方法創建線程池,因為這種方式會使用無界的任務隊列,為避免OOM,我們應該使用ThreadPoolExecutor的構造方法手動指定隊列的最大長度:

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), // 使用有界隊列,避免OOM
                new ThreadPoolExecutor.DiscardPolicy());

明確拒絕任務時的行為

任務隊列總有占滿的時候,這是再submit()提交新的任務會怎麼樣呢?RejectedExecutionHandler介面為我們提供了控制方式,介面定義如下:

public interface RejectedExecutionHandler {
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

線程池給我們提供了幾種常見的拒絕策略:
undefined

拒絕策略 拒絕行為
AbortPolicy 拋出RejectedExecutionException
DiscardPolicy 什麼也不做,直接忽略
DiscardOldestPolicy 丟棄執行隊列中最老的任務,嘗試為當前提交的任務騰出位置
CallerRunsPolicy 直接由提交任務者執行這個任務

線程池預設的拒絕行為是AbortPolicy,也就是拋出RejectedExecutionHandler異常,該異常是非受檢異常,很容易忘記捕獲。如果不關心任務被拒絕的事件,可以將拒絕策略設置成DiscardPolicy,這樣多餘的任務會悄悄的被忽略。

ExecutorService executorService = new ThreadPoolExecutor(2, 2, 
                0, TimeUnit.SECONDS, 
                new ArrayBlockingQueue<>(512), 
                new ThreadPoolExecutor.DiscardPolicy());// 指定拒絕策略

獲取處理結果和異常

線程池的處理結果、以及處理過程中的異常都被包裝到Future中,併在調用Future.get()方法時獲取,執行過程中的異常會被包裝成ExecutionExceptionsubmit()方法本身不會傳遞結果和任務執行過程中的異常。獲取執行結果的代碼可以這樣寫:

ExecutorService executorService = Executors.newFixedThreadPool(4);
Future<Object> future = executorService.submit(new Callable<Object>() {
        @Override
        public Object call() throws Exception {
            throw new RuntimeException("exception in call~");// 該異常會在調用Future.get()時傳遞給調用者
        }
    });
    
try {
  Object result = future.get();
} catch (InterruptedException e) {
  // interrupt
} catch (ExecutionException e) {
  // exception in Callable.call()
  e.printStackTrace();
}

上述代碼輸出類似如下:
undefined

線程池的常用場景

正確構造線程池

int poolSize = Runtime.getRuntime().availableProcessors() * 2;
BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(512);
RejectedExecutionHandler policy = new ThreadPoolExecutor.DiscardPolicy();
executorService = new ThreadPoolExecutor(poolSize, poolSize,
    0, TimeUnit.SECONDS,
            queue,
            policy);

獲取單個結果

submit()向線程池提交任務後會返回一個Future,調用V Future.get()方法能夠阻塞等待執行結果,V get(long timeout, TimeUnit unit)方法可以指定等待的超時時間。

獲取多個結果

如果向線程池提交了多個任務,要獲取這些任務的執行結果,可以依次調用Future.get()獲得。但對於這種場景,我們更應該使用ExecutorCompletionService,該類的take()方法總是阻塞等待某一個任務完成,然後返回該任務的Future對象。向CompletionService批量提交任務後,只需調用相同次數的CompletionService.take()方法,就能獲取所有任務的執行結果,獲取順序是任意的,取決於任務的完成順序:

void solve(Executor executor, Collection<Callable<Result>> solvers)
   throws InterruptedException, ExecutionException {
   
   CompletionService<Result> ecs = new ExecutorCompletionService<Result>(executor);// 構造器
   
   for (Callable<Result> s : solvers)// 提交所有任務
       ecs.submit(s);
       
   int n = solvers.size();
   for (int i = 0; i < n; ++i) {// 獲取每一個完成的任務
       Result r = ecs.take().get();
       if (r != null)
           use(r);
   }
}

單個任務的超時時間

V Future.get(long timeout, TimeUnit unit)方法可以指定等待的超時時間,超時未完成會拋出TimeoutException

多個任務的超時時間

等待多個任務完成,並設置最大等待時間,可以通過CountDownLatch完成:

public void testLatch(ExecutorService executorService, List<Runnable> tasks) 
    throws InterruptedException{
      
    CountDownLatch latch = new CountDownLatch(tasks.size());
      for(Runnable r : tasks){
          executorService.submit(new Runnable() {
              @Override
              public void run() {
                  try{
                      r.run();
                  }finally {
                      latch.countDown();// countDown
                  }
              }
          });
      }
      latch.await(10, TimeUnit.SECONDS); // 指定超時時間
  }

線程池和裝修公司

以運營一家裝修公司做個比喻。公司在辦公地點等待客戶來提交裝修請求;公司有固定數量的正式工以維持運轉;旺季業務較多時,新來的客戶請求會被排期,比如接單後告訴用戶一個月後才能開始裝修;當排期太多時,為避免用戶等太久,公司會通過某些渠道(比如人才市場、熟人介紹等)雇佣一些臨時工(註意,招聘臨時工是在排期排滿之後);如果臨時工也忙不過來,公司將決定不再接收新的客戶,直接拒單。

線程池就是程式中的“裝修公司”,代勞各種臟活累活。上面的過程對應到線程池上:

// Java線程池的完整構造函數
public ThreadPoolExecutor(
  int corePoolSize, // 正式工數量
  int maximumPoolSize, // 工人數量上限,包括正式工和臨時工
  long keepAliveTime, TimeUnit unit, // 臨時工游手好閑的最長時間,超過這個時間將被解雇
  BlockingQueue<Runnable> workQueue, // 排期隊列
  ThreadFactory threadFactory, // 招人渠道
  RejectedExecutionHandler handler) // 拒單方式

總結

Executors為我們提供了構造線程池的便捷方法,對於伺服器程式我們應該杜絕使用這些便捷方法,而是直接使用線程池ThreadPoolExecutor的構造方法,避免無界隊列可能導致的OOM以及線程個數限制不當導致的線程數耗盡等問題。ExecutorCompletionService提供了等待所有任務執行結束的有效方式,如果要設置等待的超時時間,則可以通過CountDownLatch完成。

參考

ThreadPoolExecutor API Doc


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 強弱類型的語言,簡單來區分就是會不會隱式轉換數據類型, 比如最常見的數值型與字元串之間的轉換 強類型的語言 : java , .net , python等 弱類型的語言: php JavaScript等 舉個python 與 JavaScript的例子 python a = 5 b = '5' pr ...
  • angularjs通過ng-change和watch兩種方式實現對錶單輸入改變的監控 ...
  • 面試中2次被問到過這個知識點,實際開發中,應用事件委托也比較常見。JS中事件委托的實現主要依賴於 事件冒泡 。那什麼是事件冒泡?就是事件從最深的節點開始,然後逐步向上傳播事件,舉個例子:頁面上有這麼一個節點樹,div>ul>li>a;比如給最裡面的a加一個click點擊事件,那麼這個事件就會一層一層 ...
  • 近年來,一些動態特性開始作為規範的一部分,出現在CSS語言中。在本文,你將學會如何使用CSS變數,並把它集成到你的CSS開發流程中,讓你的樣式表更好維護,且減少重覆。 ...
  • 我們知道關鍵字function用來定義函數;函數定義可以寫成函數定義表達式,也可以寫成語句的形式。例如下麵的兩種寫法 儘管函數聲明語句和函數定義表達式包含相同的函數名;但它們之間還是有區別的。 相同點:兩種方式都創建了新的函數對象;兩者都會被“提前”(函數語句中定義的函數被顯示的提前到腳本或則函數的 ...
  • 目錄 簡介 持久化 主從複製 高可用 Redis Sentinel .NET Core開發 分散式 Redis Cluster 配置說明 常見問題 簡介 本節內容基於 CentOS 7.4.1708,Redis 3.2.12 環境實驗。 Redis 是一個開源的高性能鍵值對資料庫。 安裝: 特性: ...
  • 近期公司在做架構梳理已經項目架構方向,不知不覺就引起了使用“work”跑數據還是用“MQ”進行跑數據的爭論! 對於爭論這件事在各行各業都有,其實我覺得針對“爭論”這個詞的根源在於一件事情有很多解決方案,每個人的認知不同, 給出的解決方案也不同。然而如果有一個對實際情況都瞭解和對解決問題有充足認知的情 ...
  • Java 語言中,無論新菜鳥,還是老司機,真正瞭解String記憶體的很少。關於String 的試題,花樣很多。== 在什麼情況下是true,什麼情況是false。我總結出如下3點讓你徹底結束對String的模糊感。無論怎麼變化,都離不開以下3種類型: 1、常量池存取(同一引用): String st ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...