前兩天一個晚上,正當我沉浸在敲代碼的快樂中時,聽到隔壁的同事傳來一聲不可置信的驚呼:線程池提交命令怎麼可能會執行一秒多?程序員
線程池提交方法執行一秒多?那不對啊,線程池提交應該是一個很快的操做,通常狀況下不該該執行一秒多那麼長的時間。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); 複製代碼
可是看了一下咱們的監控,線程數量一直比較健康,應該不是這個緣由。再說那個地方新線程也不太可能達到這個量級。工具
線程池的任務隊列是一個同步隊列。因此入隊列操做是同步的。
經常使用的幾個同步隊列:
LinkedBlockingQueue
鏈式阻塞隊列,底層數據結構是鏈表,默認大小是Integer.MAX_VALUE
,也能夠指定大小。
ArrayBlockingQueue
數組阻塞隊列,底層數據結構是數組,須要指定隊列的大小。
SynchronousQueue
同步隊列,內部容量爲0,每一個put操做必須等待一個take操做,反之亦然。
DelayQueue
延遲隊列,該隊列中的元素只有當其指定的延遲時間到了,纔可以從隊列中獲取到該元素 。
因此使用特殊的同步隊列仍是有可能致使execute
方法阻塞一秒多的,好比SynchronousQueue
。若是配合一個特殊的「拒絕策略」,是有可能形成這個現象的,咱們將在下面給出例子。
線程數量達到最大線程數就會採用拒絕處理策略,四種拒絕處理的策略爲 :
能夠看到,前面三種拒絕處理策略都是會「丟棄」任務,而最後一種不會。最後一種拒絕策略配合上面的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); } 複製代碼
因此咱們遇到的問題會是上面的種種緣由致使的嗎?帶着這些猜想,咱們去找到了定義executor的代碼。
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
executor.setConcurrencyLimit(20); 複製代碼
設置最大併發數量是20好像沒什麼問題,等等,這個SimpleAsyncTaskExecutor
是個什麼鬼?
好像是Spring提供的一個線程池吧……(聲音逐漸不自信)
em…看了一下包的定義,org.springframework.core.task,確實是Spring提供的。至因而不是線程池,先看看類圖:
實現的是Executor
接口,可是繼承樹裏爲何沒有ThreadPoolExecutor
?咱們猜想多是Spring本身實現了一個線程池?雖然應該沒什麼必要。
帶着疑問,咱們繼續看了一下這個類的源碼。主要看execute
方法,發現每次執行以前,都要先調用一個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 排版