【搞定面試官】你還在用Executors來創建線程池?會有什麼問題呢?

来源:https://www.cnblogs.com/LoveBell/archive/2019/12/03/11979958.html

前言 "上文" 我們介紹了JDK中的線程池框架 。我們知道,只要需要創建線程的情況下,即使是在單線程模式下,我們也要儘量使用 。即: 但是,在 "《阿裡巴巴Java開發手冊》" 中有一條 【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這 ...


前言

上文我們介紹了JDK中的線程池框架Executor。我們知道,只要需要創建線程的情況下,即使是在單線程模式下,我們也要儘量使用Executor。即:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(1); 
//此處不該利用Executors工具類來初始化線程池

但是,在《阿裡巴巴Java開發手冊》中有一條

【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

Executors 返回的線程池對象的弊端如下:
FixedThreadPool 和 SingleThreadPool : 允許的請求隊列長度為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允許的創建線程數量為 Integer.MAX_VALUE,可能會創建大量的線程,從而導致 OOM。

可以看到,這是一個強制性的規則,並且是不允許使用Executors來創建,建議使用ThreadPoolExecutor來創建線程池,那我們先來回顧一下ExecutorsThreadPoolExecutor

我們可以看到ThreadPoolExecutor已經是Executor的具體實現了,而且具有較多可配參數(可配參數見下方,可僅瞭解,用到時再進行詳細查詢)。Executors是一個創建線程池的工具類,查看其源碼的話也會發現這幾種創建線程池的方法也都是通過調用ThreadPoolExecutor來實現的。

ThreadPoolExecutor一共有四個構造函數,七個可配參數,分別是

  1. corePoolSize: 線程池中保持存活線程的數量。
  2. maximumPoolSize: 線程池中允許線程數量的最大值
  3. keepAliveTime: 表示線程沒有任務執行時最多保持多久時間會終止
  4. unit: 參數keepAliveTime的時間單位
  5. workQueue: 一個阻塞隊列,用來存儲等待執行的任務
  6. threadFactory: 線程工廠,主要用來創建線程
  7. handler:表示當拒絕處理任務時的策略

分析

那麼Executors到底會導致什麼問題,才會讓開發手冊中直接被定義為不允許了呢。首先就是一個血淋淋的教訓,直接導致線上服務不可用,已經可以算是事故了。

實驗

我們也可以現在我們本地進行一下小實驗:

public class ExecutorsTesting {
    private static ExecutorService executor = Executors.newFixedThreadPool(15);
    public static void main(String[] args) {
        for (int i = 0; i < Integer.MAX_VALUE; i++) {
            executor.execute(new SubThread());
        }
    }
}

class SubThread implements Runnable {
    @Override
    public void run() {
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            //do nothing
        }
    }
}

運行時指定JVM參數:-Xmx8m -Xms8m,大概幾秒鐘之後,會報出OOM錯誤:

Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
    at com.kaikeba.mybatis.ExecutorsTesting.main(ExecutorsTesting.java:10) 
    //報錯行數為上述代碼中的executor.execute(new SubThread());

那麼為什麼會報出這個錯誤呢。

源碼分析

我們先來看一下Executors中的FixedThreadPool是如何構造的。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

可以看到對於存儲等待執行的任務,FixedThreadPool是通過LinkedBlockingQueue來實現的。而我們知道LinkedBlockingQueue是一個鏈表實現的阻塞隊列,而如果不設置其容量的話,將會是一個無邊界的阻塞隊列,最大長度為Integer.MAX_VALUE由於Executors中並未設置容量,所以應用可以不斷向隊列中添加任務,導致OOM錯誤

上面提到的問題主要體現在newFixedThreadPoolnewSingleThreadExecutor兩個工廠方法上,並不是說newCachedThreadPoolnewScheduledThreadPool這兩個方法就安全了,這兩種方式創建的最大線程數可能是Integer.MAX_VALUE,而創建這麼多線程,必然就有可能導致OOM。

如何該利用ThreadPoolExecutor來創建線程池呢?

我們其實可以看到Executors中的newFixedThreadPool其實也是調用ThreadPoolExecutor來實現的。正如手冊中所說,當我們不用Executors預設創建線程池的方法,而直接自己手動去調用ThreadPoolExecutor,可以讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。比如我們在Executors.newFixedThreadPool基礎上給LinkedBlockingQueue加一個容量,當隊列已經滿了,而仍需要添加新的請求會拋出相應異常,我們可以根據異常做相應處理。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(10)); //添加容量大小
}

除了自己定義ThreadPoolExecutor外。還可以利用其它開源類庫,如apache和guava等,可以有更多個性化配置。

參考文章:

https://www.hollischuang.com/archives/2888

https://stackoverflow.com/questions/1094867/when-should-we-use-javas-thread-over-executor#answer-34373289

https://blog.51cto.com/zero01/2306857


本文由博客一文多發平臺 OpenWrite 發佈!

文章首發:https://zhuanlan.zhihu.com/lovebell

個人公眾號:技術Go

您的點贊與支持是作者持續更新的最大動力!


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

更多相關文章
  • 命令模式,正如模式的名字一樣,該模式中的不同操作都可以當做不同的命令來執行,可以使用隊列來執行一系列的命令,也可以單獨執行某個命令。該模式重點是將不同的操作封裝為不同的命令對象,將操作的調用者與執行者進行解耦。 命令模式中的Command對象(即每一個命令,或者說命令對象)用於封裝在完成某項操作或觸 ...
  • pip install redis 五大數據類型使用 String類型 List類型 Hash類型 Set類型 Zset類型 全局key操作 ...
  • LinkedList實現原理(JDK1.8) LinkedList底層採用雙向鏈表,如果對鏈表這種結構比較熟悉的話,那LinkedList的實現原理看明白就相當容易。 鏈表通過“指針”將一組零散的記憶體塊串聯起來使用,每一個元素(節點)通過指針指向它的下一個元素,最後一個節點的下一個指向為null,而 ...
  • Java抽象類: 抽象類特點 :抽象類除了不能實例化對象之外,類的其它功能依然存在,成員變數、成員方法和構造方法的訪問方式和普通類一樣。 由於抽象類不能實例化對象,所以抽象類必須被extends [抽象類]方式 繼承才能被使用。 抽象類表示的是一種繼承關係。 (總結就是:抽象類裡面設計跟普通類一樣, ...
  • lua中json和table的互轉,是我們在平時開發過程中經常用到的。比如: 在用lua編寫的伺服器中,如果客戶端發送json格式的數據,那麼在lua處理業務邏輯的時候,必然需要轉換成lua自己的數據結構,如table。此時,就會用到table和json格式的互轉。 在用lua編寫的伺服器中,如果我 ...
  • 先定義消息類型 orders.proto 在GOPATH創建目錄和編譯這個消息類型輸出到該目錄,包名是message 編寫go文件進行序列化和反序列化剛纔生成的包里的類型結構體數據 ...
  • swap(a,b) 用於交換a,b兩個變數的值; max(a,b) 返回a,b中的最大值; min(a,b) 返回a,b中的最小值; abs(x) 返回x的絕對值,x必須是整數; ...
  • ——日拱一卒,不期而至! 你好,我是彤哥,本篇是netty系列的第六篇。 簡介 上一章我們一起學習了Java NIO的核心組件Channel,它可以看作是實體與實體之間的連接,而且需要與Buffer交互,這一章我們就來學習一下Buffer的特性。 概念 Buffer用於與Channel交互時使用,通 ...
一周排行
  • FastDBF源代碼地址:https://github.com/SocialExplorer/FastDBF 第一步在解決方案中新建一個類庫的項目:取名為SocialExplorer.FastDBF 第二步:引入FASTDBF的源文件 源代碼可以通過github地址下載引入 源文件:DbfColum ...
  • 目 錄 1. 概述... 2 2. 演示信息... 2 3. 安裝Docker容器... 2 4. 安裝dotnet鏡像... 3 5. 複製iNeuKernel到容器中... 4 6. 進入指定容器... 4 7. 安裝dotnet框架... 4 8. 在Docker容器中運行iNeuKernel ...
  • 本筆記摘抄自:https://www.cnblogs.com/PatrickLiu/p/7699301.html,記錄一下學習過程以備後續查用。 一、引言 今天我們要講結構型設計模式的第二個模式--橋接模式,也有叫橋模式的。橋在我們現實生活中經常是連接著A地和B地,再往後來發展,橋引申為一種紐 帶, ...
  • static void AggregateExceptionsDemo() { var task1 = Task.Factory.StartNew(() => { var child1 = Task.Factory.StartNew(() => { throw new CustomException ...
  • http請求在我們實際工作中天天見,為了不重覆造輪子,現在分享一下最近的一次封裝整理,供大家參考,交流,學習! ...
  • 隨著你的 Python 項目越來越多,你會發現不同的項目會需要 不同的版本的 Python 庫。同一個 Python 庫的不同版本可能不相容。虛擬環境可以為每一個項目安裝獨立的 Python 庫,這樣就可以隔離不同項目之間的 Python 庫,也可以隔離項目與操作系統之間的 Python 庫。 1. ...
  • TypeScript 介紹 TypeScript 是什麼 TypeScript 是 JavaScript 的強類型版本。然後在編譯期去掉類型和特有語法,生成純粹的 JavaScript 代碼。由於最終在瀏覽器中運行的仍然是 JavaScript,所以 TypeScript 並不依賴於瀏覽器的支持,也 ...
  • Hello World 新建 並寫入以下內容: 安裝編譯器: 編譯: 修改 文件中的代碼,為 greeter 函數的參數 person 加上類型聲明 : 重新編譯執行。 讓我們繼續修改: 重新編譯,你將看到如下錯誤: 介面(Interface) 類(Class) 變數聲明 作用域 重覆聲明 塊級作用 ...
  • 解構賦值 數組解構 上面的寫法等價於: 利用解構賦值交換變數: 函數參數解構: 解構剩餘參數: 也可以忽略其它參數: 或者跳過解構: 對象解構 示例一: 就像數組解構,你可以用沒有聲明的賦值: 你可以在對象里使用 語法創建剩餘變數: 屬性解構重命名 你也可以給屬性以不同的名字: 註意,這裡的冒號 不 ...
  • 函數 函數參數 參數及返回值類型 可選參數 預設參數 剩餘參數 箭頭函數 基本示例 for of 迴圈 for 迴圈 forEach 不支持 break for in 會把數組當作對象來遍歷 for of 支持 break 類型推斷(Type Inference) 類型相容性 模塊 概念 模塊通信: ...
x