線程池最佳實踐!安排!

你們好,我是 Guide 哥,一個三觀比主角還正的技術人。今天再來繼續聊聊線程池~html

線程池最佳實踐

這篇文章篇幅雖短,可是絕對是乾貨。標題稍微有點誇張,嘿嘿,實際都是本身使用線程池的時候總結的一些我的感受比較重要的點。java

線程池知識回顧

開始這篇文章以前仍是簡單介紹一嘴線程池,以前寫的《新手也能看懂的線程池學習總結》這篇文章介紹的很詳細了。git

爲何要使用線程池?

池化技術相比你們已經家常便飯了,線程池、數據庫鏈接池、Http 鏈接池等等都是對這個思想的應用。池化技術的思想主要是爲了減小每次獲取資源的消耗,提升對資源的利用率。github

線程池提供了一種限制和管理資源(包括執行一個任務)。 每一個線程池還維護一些基本統計信息,例如已完成任務的數量。web

這裏借用《Java 併發編程的藝術》提到的來講一下使用線程池的好處面試

  • 下降資源消耗。經過重複利用已建立的線程下降線程建立和銷燬形成的消耗。
  • 提升響應速度。當任務到達時,任務能夠不須要的等到線程建立就能當即執行。
  • 提升線程的可管理性。線程是稀缺資源,若是無限制的建立,不只會消耗系統資源,還會下降系統的穩定性,使用線程池能夠進行統一的分配,調優和監控。

線程池在實際項目的使用場景

線程池通常用於執行多個不相關聯的耗時任務,沒有多線程的狀況下,任務順序執行,使用了線程池的話可以讓多個不相關聯的任務同時執行。數據庫

假設咱們要執行三個不相關的耗時任務,Guide 畫圖給你們展現了使用線程池先後的區別。編程

注意:下面三個任務可能作的是同一件事情,也多是不同的事情。微信

使用線程池先後對比
使用線程池先後對比

如何使用線程池?

通常是經過 ThreadPoolExecutor 的構造函數來建立線程池,而後提交任務給線程池執行就能夠了。網絡

ThreadPoolExecutor構造函數以下:

/**  * 用給定的初始參數建立一個新的ThreadPoolExecutor。  */  public ThreadPoolExecutor(int corePoolSize,//線程池的核心線程數量  int maximumPoolSize,//線程池的最大線程數  long keepAliveTime,//當線程數大於核心線程數時,多餘的空閒線程存活的最長時間  TimeUnit unit,//時間單位  BlockingQueue<Runnable> workQueue,//任務隊列,用來儲存等待執行任務的隊列  ThreadFactory threadFactory,//線程工廠,用來建立線程,通常默認便可  RejectedExecutionHandler handler//拒絕策略,當提交的任務過多而不能及時處理時,咱們能夠定製策略來處理任務  ) {  if (corePoolSize < 0 ||  maximumPoolSize <= 0 ||  maximumPoolSize < corePoolSize ||  keepAliveTime < 0)  throw new IllegalArgumentException();  if (workQueue == null || threadFactory == null || handler == null)  throw new NullPointerException();  this.corePoolSize = corePoolSize;  this.maximumPoolSize = maximumPoolSize;  this.workQueue = workQueue;  this.keepAliveTime = unit.toNanos(keepAliveTime);  this.threadFactory = threadFactory;  this.handler = handler;  } 複製代碼

簡單演示一下如何使用線程池,更詳細的介紹,請看:《新手也能看懂的線程池學習總結》

private static final int CORE_POOL_SIZE = 5;
 private static final int MAX_POOL_SIZE = 10;  private static final int QUEUE_CAPACITY = 100;  private static final Long KEEP_ALIVE_TIME = 1L;   public static void main(String[] args) {   //使用阿里巴巴推薦的建立線程池的方式  //經過ThreadPoolExecutor構造函數自定義參數建立  ThreadPoolExecutor executor = new ThreadPoolExecutor(  CORE_POOL_SIZE,  MAX_POOL_SIZE,  KEEP_ALIVE_TIME,  TimeUnit.SECONDS,  new ArrayBlockingQueue<>(QUEUE_CAPACITY),  new ThreadPoolExecutor.CallerRunsPolicy());   for (int i = 0; i < 10; i++) {  executor.execute(() -> {  try {  Thread.sleep(2000);  } catch (InterruptedException e) {  e.printStackTrace();  }  System.out.println("CurrentThread name:" + Thread.currentThread().getName() + "date:" + Instant.now());  });  }  //終止線程池  executor.shutdown();  try {  executor.awaitTermination(5, TimeUnit.SECONDS);  } catch (InterruptedException e) {  e.printStackTrace();  }  System.out.println("Finished all threads");  } 複製代碼

控制檯輸出:

CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:31.639Z
CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:31.639Z CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:31.636Z CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:31.639Z CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:31.639Z CurrentThread name:pool-1-thread-2date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-4date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-1date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-3date:2020-06-06T11:45:33.656Z CurrentThread name:pool-1-thread-5date:2020-06-06T11:45:33.656Z Finished all threads 複製代碼

線程池最佳實踐

簡單總結一下我瞭解的使用線程池的時候應該注意的東西,網上彷佛尚未專門寫這方面的文章。

由於 Guide 還比較菜,有補充和完善的地方,能夠在評論區告知或者在微信上與我交流。

1. 使用 ThreadPoolExecutor 的構造函數聲明線程池

1. 線程池必須手動經過 ThreadPoolExecutor 的構造函數來聲明,避免使用Executors 類的 newFixedThreadPoolnewCachedThreadPool ,由於可能會有 OOM 的風險。

Executors 返回線程池對象的弊端以下:

  • FixedThreadPoolSingleThreadExecutor : 容許請求的隊列長度爲 Integer.MAX_VALUE,可能堆積大量的請求,從而致使 OOM。
  • CachedThreadPool 和 ScheduledThreadPool : 容許建立的線程數量爲 Integer.MAX_VALUE ,可能會建立大量線程,從而致使 OOM。

說白了就是:使用有界隊列,控制線程建立數量。

除了避免 OOM 的緣由以外,不推薦使用 Executors提供的兩種快捷的線程池的緣由還有:

  1. 實際使用中須要根據本身機器的性能、業務場景來手動配置線程池的參數好比核心線程數、使用的任務隊列、飽和策略等等。
  2. 咱們應該顯示地給咱們的線程池命名,這樣有助於咱們定位問題。

2.監測線程池運行狀態

你能夠經過一些手段來檢測線程池的運行狀態好比 SpringBoot 中的 Actuator 組件。

除此以外,咱們還能夠利用 ThreadPoolExecutor 的相關 API 作一個簡陋的監控。從下圖能夠看出, ThreadPoolExecutor提供了線程池當前的線程數和活躍線程數、已經執行完成的任務數、正在排隊中的任務數等等。

下面是一個簡單的 Demo。printThreadPoolStatus()會每隔一秒打印出線程池的線程數、活躍線程數、完成的任務數、以及隊列中的任務數。

/**  * 打印線程池的狀態  *  * @param threadPool 線程池對象  */  public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {  ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-thread-pool-status", false));  scheduledExecutorService.scheduleAtFixedRate(() -> {  log.info("=========================");  log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());  log.info("Active Threads: {}", threadPool.getActiveCount());  log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());  log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());  log.info("=========================");  }, 0, 1, TimeUnit.SECONDS);  } 複製代碼

3.建議不一樣類別的業務用不一樣的線程池

不少人在實際項目中都會有相似這樣的問題:個人項目中多個業務須要用到線程池,是爲每一個線程池都定義一個仍是說定義一個公共的線程池呢?

通常建議是不一樣的業務使用不一樣的線程池,配置線程池的時候根據當前業務的狀況對當前線程池進行配置,由於不一樣的業務的併發以及對資源的使用狀況都不一樣,重心優化系統性能瓶頸相關的業務。

咱們再來看一個真實的事故案例! (本案例來源自:《線程池運用不當的一次線上事故》 ,很精彩的一個案例)

案例代碼概覽
案例代碼概覽

上面的代碼可能會存在死鎖的狀況,爲何呢?畫個圖給你們捋一捋。

試想這樣一種極端狀況:

假如咱們線程池的核心線程數爲 n,父任務(扣費任務)數量爲 n,父任務下面有兩個子任務(扣費任務下的子任務),其中一個已經執行完成,另一個被放在了任務隊列中。因爲父任務把線程池核心線程資源用完,因此子任務由於沒法獲取到線程資源沒法正常執行,一直被阻塞在隊列中。父任務等待子任務執行完成,而子任務等待父任務釋放線程池資源,這也就形成了 "死鎖"

解決方法也很簡單,就是新增長一個用於執行子任務的線程池專門爲其服務。

4.別忘記給線程池命名

初始化線程池的時候須要顯示命名(設置線程池名稱前綴),有利於定位問題。

默認狀況下建立的線程名字相似 pool-1-thread-n 這樣的,沒有業務含義,不利於咱們定位問題。

給線程池裏的線程命名一般有下面兩種方式:

**1.利用 guava 的 ThreadFactoryBuilder **

ThreadFactory threadFactory = new ThreadFactoryBuilder()
 .setNameFormat(threadNamePrefix + "-%d")  .setDaemon(true).build(); ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory) 複製代碼

2.本身實現 ThreadFactor

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; /**  * 線程工廠,它設置線程名稱,有利於咱們定位問題。  */ public final class NamingThreadFactory implements ThreadFactory {   private final AtomicInteger threadNum = new AtomicInteger();  private final ThreadFactory delegate;  private final String name;   /**  * 建立一個帶名字的線程池生產工廠  */  public NamingThreadFactory(ThreadFactory delegate, String name) {  this.delegate = delegate;  this.name = name; // TODO consider uniquifying this  }   @Override  public Thread newThread(Runnable r) {  Thread t = delegate.newThread(r);  t.setName(name + " [#" + threadNum.incrementAndGet() + "]");  return t;  }  } 複製代碼

5.正確配置線程池參數

說到如何給線程池配置參數,美團的騷操做至今讓我難忘(後面會提到)!

咱們先來看一下各類書籍和博客上通常推薦的配置線程池參數的方式,能夠做爲參考!

常規操做

不少人甚至可能都會以爲把線程池配置過大一點比較好!我以爲這明顯是有問題的。就拿咱們生活中很是常見的一例子來講:並非人多就能把事情作好,增長了溝通交流成本。你原本一件事情只須要 3 我的作,你硬是拉來了 6 我的,會提高作事效率嘛?我想並不會。 線程數量過多的影響也是和咱們分配多少人作事情同樣,對於多線程這個場景來講主要是增長了上下文切換成本。不清楚什麼是上下文切換的話,能夠看我下面的介紹。

上下文切換:

多線程編程中通常線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能獲得有效執行,CPU 採起的策略是爲每一個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會從新處於就緒狀態讓給其餘線程使用,這個過程就屬於一次上下文切換。歸納來講就是:當前任務在執行完 CPU 時間片切換到另外一個任務以前會先保存本身的狀態,以便下次再切換回這個任務時,能夠再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換

上下文切換一般是計算密集型的。也就是說,它須要至關可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都須要納秒量級的時間。因此,上下文切換對系統來講意味着消耗大量的 CPU 時間,事實上,多是操做系統中時間消耗最大的操做。

Linux 相比與其餘操做系統(包括其餘類 Unix 系統)有不少的優勢,其中有一項就是,其上下文切換和模式切換的時間消耗很是少。

類比於實現世界中的人類經過合做作某件事情,咱們能夠確定的一點是線程池大小設置過大或者太小都會有問題,合適的纔是最好。

若是咱們設置的線程池數量過小的話,若是同一時間有大量任務/請求須要處理,可能會致使大量的請求/任務在任務隊列中排隊等待執行,甚至會出現任務隊列滿了以後任務/請求沒法處理的狀況,或者大量任務堆積在任務隊列致使 OOM。這樣很明顯是有問題的! CPU 根本沒有獲得充分利用。

可是,若是咱們設置線程數量太大,大量線程可能會同時在爭取 CPU 資源,這樣會致使大量的上下文切換,從而增長線程的執行時間,影響了總體執行效率。

有一個簡單而且適用面比較廣的公式:

  • CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,能夠將線程數設置爲 N(CPU 核心數)+1,比 CPU 核心數多出來的一個線程是爲了防止線程偶發的缺頁中斷,或者其它緣由致使的任務暫停而帶來的影響。一旦任務暫停,CPU 就會處於空閒狀態,而在這種狀況下多出來的一個線程就能夠充分利用 CPU 的空閒時間。
  • I/O 密集型任務(2N): 這種任務應用起來,系統會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內不會佔用 CPU 來處理,這時就能夠將 CPU 交出給其它線程使用。所以在 I/O 密集型任務的應用中,咱們能夠多配置一些線程,具體的計算方法是 2N。

如何判斷是 CPU 密集任務仍是 IO 密集任務?

CPU 密集型簡單理解就是利用 CPU 計算能力的任務好比你在內存中對大量數據進行排序。單凡涉及到網絡讀取,文件讀取這類都是 IO 密集型,這類任務的特色是 CPU 計算耗費時間相比於等待 IO 操做完成的時間來講不多,大部分時間都花在了等待 IO 操做完成上。

美團的騷操做

美團技術團隊在《Java 線程池實現原理及其在美團業務中的實踐》這篇文章中介紹到對線程池參數實現可自定義配置的思路和方法。

美團技術團隊的思路是主要對線程池的核心參數實現自定義可配置。這三個核心參數是:

  • corePoolSize : 核心線程數線程數定義了最小能夠同時運行的線程數量。
  • maximumPoolSize : 當隊列中存放的任務達到隊列容量的時候,當前能夠同時運行的線程數量變爲最大線程數。
  • workQueue: 當新任務來的時候會先判斷當前運行的線程數量是否達到核心線程數,若是達到的話,信任就會被存放在隊列中。

爲何是這三個參數?

我在這篇《新手也能看懂的線程池學習總結》 中就說過這三個參數是 ThreadPoolExecutor 最重要的參數,它們基本決定了線程池對於任務的處理策略。

如何支持參數動態配置? 且看 ThreadPoolExecutor 提供的下面這些方法。

格外須要注意的是corePoolSize, 程序運行期間的時候,咱們調用 setCorePoolSize()這個方法的話,線程池會首先判斷當前工做線程數是否大於corePoolSize,若是大於的話就會回收工做線程。

另外,你也看到了上面並無動態指定隊列長度的方法,美團的方式是自定義了一個叫作 ResizableCapacityLinkedBlockIngQueue 的隊列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 關鍵字修飾給去掉了,讓它變爲可變的)。

最終實現的可動態修改線程池參數效果以下。👏👏👏

動態配置線程池參數最終效果
動態配置線程池參數最終效果

還沒看夠?推薦 why 神的《如何設置線程池參數?美團給出了一個讓面試官虎軀一震的回答。》這篇文章,深度剖析,很不錯哦!

做者介紹: Github 80k Star 項目 JavaGuide(公衆號同名) 做者。每週都會在公衆號更新一些本身原創乾貨。公衆號後臺回覆「1」領取Java工程師必備學習資料+面試突擊pdf。

本文使用 mdnice 排版

相關文章
相關標籤/搜索