你也被Spring的這個「線程池」坑過嗎?

前兩天一個晚上,正當我沉浸在敲代碼的快樂中時,聽到隔壁的同事傳來一聲不可置信的驚呼:線程池提交命令怎麼可能會執行一秒多?程序員

線程池提交方法執行一秒多?那不對啊,線程池提交應該是一個很快的操做,通常狀況下不該該執行一秒多那麼長的時間。web

看了一下那段代碼,好像也沒什麼問題,就是一個簡單的提交任務的代碼。spring

executor.execute( () -> {
 // 具體的任務代碼  // 這裏有個for循環 }); 複製代碼

雖然執行的Job裏面有一個for循環,可能比較耗時,可是execute提交任務的時候,並不會去真正去執行Job,因此應該不是這個緣由引發的。數組

分析

看到這個狀況,咱們首先想到的是線程池提交任務時候的一個處理過程:微信

線程池原理圖
線程池原理圖

而後逐個分析一下有可能耗時一秒多的操做:數據結構

建立線程耗時?

根據上面的圖,咱們能夠知道,若是核心線程數量設置過大,就可能會不斷建立新的核心線程去執行任務。同理,若是核心線程池和任務隊列都滿了,會建立非核心線程去執行任務。併發

建立線程是比較耗時的,並且Java線程池在這裏建立線程的時候還上了鎖。編輯器

final ReentrantLock mainLock = this.mainLock;
mainLock.lock(); 複製代碼

咱們寫個簡單的程序,能夠模擬出來線程池耗時的操做,下面這段代碼建立2w個線程,在個人電腦裏大概會耗時6k多毫秒。ide

long before = System.currentTimeMillis();
for (int i = 0; i < 20000; i++) {  // doSomething裏面睡眠一秒  new Thread(() -> doSomething()).start(); } long after = System.currentTimeMillis(); // 下面這行在個人電腦裏輸出6139 System.out.println(after - before); 複製代碼

可是看了一下咱們的監控,線程數量一直比較健康,應該不是這個緣由。再說那個地方新線程也不太可能達到這個量級。工具

入任務隊列耗時?

線程池的任務隊列是一個同步隊列。因此入隊列操做是同步的。

經常使用的幾個同步隊列:

  1. LinkedBlockingQueue

    鏈式阻塞隊列,底層數據結構是鏈表,默認大小是Integer.MAX_VALUE,也能夠指定大小。

  2. ArrayBlockingQueue

    數組阻塞隊列,底層數據結構是數組,須要指定隊列的大小。

  3. SynchronousQueue

    同步隊列,內部容量爲0,每一個put操做必須等待一個take操做,反之亦然。

  4. DelayQueue

    延遲隊列,該隊列中的元素只有當其指定的延遲時間到了,纔可以從隊列中獲取到該元素 。

因此使用特殊的同步隊列仍是有可能致使execute方法阻塞一秒多的,好比SynchronousQueue。若是配合一個特殊的「拒絕策略」,是有可能形成這個現象的,咱們將在下面給出例子。

拒絕策略?

線程數量達到最大線程數就會採用拒絕處理策略,四種拒絕處理的策略爲 :

  1. ThreadPoolExecutor.AbortPolicy:默認拒絕處理策略,丟棄任務並拋出異常。
  2. ThreadPoolExecutor.DiscardPolicy:丟棄新來的任務,可是不拋出異常。
  3. ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列頭部(最舊的)的任務,而後從新嘗試執行程序(若是再次失敗,重複此過程)。
  4. ThreadPoolExecutor.CallerRunsPolicy:由調用線程處理該任務。

能夠看到,前面三種拒絕處理策略都是會「丟棄」任務,而最後一種不會。最後一種拒絕策略配合上面的SynchronousQueue,就有可能形成咱們遇到的狀況。示例代碼:

Executor executor = new ThreadPoolExecutor(2,2, 2, 
 TimeUnit.MILLISECONDS,new SynchronousQueue<>(),  new ThreadPoolExecutor.CallerRunsPolicy()); for (int i = 0; i < 3; i++) {  long before = System.currentTimeMillis();  executor.execute( () -> {  // doSomething裏面睡眠一秒  doSomething();  });  long after = System.currentTimeMillis();  // 下面這段代碼,第三行會輸出1001  System.out.println(after - before); } 複製代碼

SimpleAsyncTaskExecutor

因此咱們遇到的問題會是上面的種種緣由致使的嗎?帶着這些猜想,咱們去找到了定義executor的代碼。

SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setConcurrencyLimit(20); 複製代碼

設置最大併發數量是20好像沒什麼問題,等等,這個SimpleAsyncTaskExecutor是個什麼鬼?

好像是Spring提供的一個線程池吧……(聲音逐漸不自信)

em…看了一下包的定義,org.springframework.core.task,確實是Spring提供的。至因而不是線程池,先看看類圖:

實現的是Executor接口,可是繼承樹裏爲何沒有ThreadPoolExecutor?咱們猜想多是Spring本身實現了一個線程池?雖然應該沒什麼必要。

源碼

帶着疑問,咱們繼續看了一下這個類的源碼。主要看execute方法,發現每次執行以前,都要先調用一個beforeAccess方法,這個方法裏面有這樣一段很奇怪的代碼:

beforeAccess
beforeAccess

while循環去檢查,若是當前併發線程數量大於等於設置的最大值,就等待。

找到緣由了,這應該就是罪魁禍首。但是爲何Spring要這麼設計呢?

咱們在SimpleAsyncTaskExecutor類的註釋上面找到了做者的留言:

* <p><b>NOTE: This implementation does not reuse threads!</b> Consider a
 * thread-pooling TaskExecutor implementation instead, in particular for  * executing a large number of short-lived tasks. 複製代碼

大概意思就是:這個實現並不複用線程,若是你要複用線程請去使用線程池的實現。這個是用來執行不少耗時很短的任務的。

至此,真相大白。

反思

使用接口前先了解一下

形成這個問題的根本緣由是,咱們覺得SimpleAsyncTaskExecutor是一個「線程池」,而其實它不是!!!

咱們在使用開源項目的時候,每每直接就用了,不會去仔細看看它的源碼,也可能沒有考慮清楚它的應用環境。等到程序出問題了才發現,已經晚了。

因此使用接口以前最好先了解一下,至少要看看官方文檔或者接口文檔/註釋。

哪怕是真的出問題了,看源碼也不失爲一種排查問題的方式,由於代碼都是死的,它不會騙人。

代碼規約

阿里有這麼一個代碼規約:不建議咱們直接使用Executors類中的線程池,而是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同窗須要更加明確線程池的運行規則,規避資源耗盡的風險。

之前我還不太理解,心想使用Executors類能夠提升可讀性,JDK提供了這樣的工具類,不用白不用。直到遇到這個問題,才明白這條規約的良苦用心。

若是咱們使用規範的方式去使用線程池,而不是用一個所謂的Spring提供的「線程池」,就不會遇到這個問題了。

明確接口職責

再來想想爲何同事會把它當成一個線程池?由於它的類名、方法名都太像一個線程池了。它實現了Executor接口的execute方法,才致使咱們誤覺得它是一個線程池。

因此迴歸到Executor這個接口上來,它的職責到底是什麼?咱們能夠在JDK的execute方法上看到這個註釋:

/** * Executes the given command at some time in the future. The command * may execute in a new thread, in a pooled thread, or in the calling * thread, at the discretion of the {@code Executor} implementation. */ 複製代碼

大意就是,在未來某個時間執行傳入的命令,這個命令可能會在一個新的線程裏面執行,可能會在線程池裏,也可能在調用這個方法的線程中,具體怎麼執行是由實現類去決定的。

因此這纔是Executor這個類的職責,它的職責並非提供一個線程池的接口,而是提供一個「未來執行命令」的接口。

因此,真正能表明線程池意義的,是ThreadPoolExecutor類,而不是Executor接口。

在咱們寫代碼的時候,也要定義清楚接口的職責喲。這樣別人用你的接口或者閱讀源碼的時候,纔不會疑惑。

關於做者

我是Yasin,一個有顏有料又有趣的程序員。

微信公衆號:編了個程

我的網站:https://yasinshaw.com

關注個人公衆號,和我一塊兒成長~

公衆號
公衆號

本文使用 mdnice 排版

相關文章
相關標籤/搜索