javascript
你也被Spring的这个“线程池”坑过吗?
前兩天一個晚上,正當(dāng)我沉浸在敲代碼的快樂中時,聽到隔壁的同事傳來一聲不可置信的驚呼:線程池提交命令怎么可能會執(zhí)行一秒多?
線程池提交方法執(zhí)行一秒多?那不對啊,線程池提交應(yīng)該是一個很快的操作,一般情況下不應(yīng)該執(zhí)行一秒多那么長的時間。
看了一下那段代碼,好像也沒什么問題,就是一個簡單的提交任務(wù)的代碼。
executor.execute(?()?->?{//?具體的任務(wù)代碼//?這里有個for循環(huán) });雖然執(zhí)行的Job里面有一個for循環(huán),可能比較耗時,但是execute提交任務(wù)的時候,并不會去真正去執(zhí)行Job,所以應(yīng)該不是這個原因引起的。
分析
看到這個情況,我們首先想到的是線程池提交任務(wù)時候的一個處理過程:
線程池原理圖然后逐個分析一下有可能耗時一秒多的操作:
創(chuàng)建線程耗時?
根據(jù)上面的圖,我們可以知道,如果核心線程數(shù)量設(shè)置過大,就可能會不斷創(chuàng)建新的核心線程去執(zhí)行任務(wù)。同理,如果核心線程池和任務(wù)隊列都滿了,會創(chuàng)建非核心線程去執(zhí)行任務(wù)。
創(chuàng)建線程是比較耗時的,而且Java線程池在這里創(chuàng)建線程的時候還上了鎖。
final?ReentrantLock?mainLock?=?this.mainLock; mainLock.lock();我們寫個簡單的程序,可以模擬出來線程池耗時的操作,下面這段代碼創(chuàng)建2w個線程,在我的電腦里大概會耗時6k多毫秒。
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);但是看了一下我們的監(jiān)控,線程數(shù)量一直比較健康,應(yīng)該不是這個原因。再說那個地方新線程也不太可能達(dá)到這個量級。
入任務(wù)隊列耗時?
線程池的任務(wù)隊列是一個同步隊列。所以入隊列操作是同步的。
常用的幾個同步隊列:
LinkedBlockingQueue
鏈?zhǔn)阶枞犃?#xff0c;底層數(shù)據(jù)結(jié)構(gòu)是鏈表,默認(rèn)大小是Integer.MAX_VALUE,也可以指定大小。
ArrayBlockingQueue
數(shù)組阻塞隊列,底層數(shù)據(jù)結(jié)構(gòu)是數(shù)組,需要指定隊列的大小。
SynchronousQueue
同步隊列,內(nèi)部容量為0,每個put操作必須等待一個take操作,反之亦然。
DelayQueue
延遲隊列,該隊列中的元素只有當(dāng)其指定的延遲時間到了,才能夠從隊列中獲取到該元素 。
所以使用特殊的同步隊列還是有可能導(dǎo)致execute方法阻塞一秒多的,比如SynchronousQueue。如果配合一個特殊的“拒絕策略”,是有可能造成這個現(xiàn)象的,我們將在下面給出例子。
拒絕策略?
線程數(shù)量達(dá)到最大線程數(shù)就會采用拒絕處理策略,四種拒絕處理的策略為 :
ThreadPoolExecutor.AbortPolicy:默認(rèn)拒絕處理策略,丟棄任務(wù)并拋出異常。
ThreadPoolExecutor.DiscardPolicy:丟棄新來的任務(wù),但是不拋出異常。
ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列頭部(最舊的)的任務(wù),然后重新嘗試執(zhí)行程序(如果再次失敗,重復(fù)此過程)。
ThreadPoolExecutor.CallerRunsPolicy:由調(diào)用線程處理該任務(wù)。
可以看到,前面三種拒絕處理策略都是會“丟棄”任務(wù),而最后一種不會。最后一種拒絕策略配合上面的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();//?下面這段代碼,第三行會輸出1001System.out.println(after?-?before); }SimpleAsyncTaskExecutor
所以我們遇到的問題會是上面的種種原因?qū)е碌膯?#xff1f;帶著這些猜測,我們?nèi)フ业搅硕xexecutor的代碼。
SimpleAsyncTaskExecutor?executor?=?new?SimpleAsyncTaskExecutor(); executor.setConcurrencyLimit(20);設(shè)置最大并發(fā)數(shù)量是20好像沒什么問題,等等,這個SimpleAsyncTaskExecutor是個什么鬼?
好像是Spring提供的一個線程池吧……(聲音逐漸不自信)
em…看了一下包的定義,org.springframework.core.task,確實是Spring提供的。至于是不是線程池,先看看類圖:
實現(xiàn)的是Executor接口,但是繼承樹里為什么沒有ThreadPoolExecutor?我們猜測可能是Spring自己實現(xiàn)了一個線程池?雖然應(yīng)該沒什么必要。
源碼
帶著疑問,我們繼續(xù)看了一下這個類的源碼。主要看execute方法,發(fā)現(xiàn)每次執(zhí)行之前,都要先調(diào)用一個beforeAccess方法,這個方法里面有這樣一段很奇怪的代碼:
beforeAccesswhile循環(huán)去檢查,如果當(dāng)前并發(fā)線程數(shù)量大于等于設(shè)置的最大值,就等待。
找到原因了,這應(yīng)該就是罪魁禍?zhǔn)住?墒菫槭裁碨pring要這么設(shè)計呢?
我們在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.大概意思就是:這個實現(xiàn)并不復(fù)用線程,如果你要復(fù)用線程請去使用線程池的實現(xiàn)。這個是用來執(zhí)行很多耗時很短的任務(wù)的。
至此,真相大白。
反思
使用接口前先了解一下
造成這個問題的根本原因是,我們以為SimpleAsyncTaskExecutor是一個“線程池”,而其實它不是!!!
我們在使用開源項目的時候,往往直接就用了,不會去仔細(xì)看看它的源碼,也可能沒有考慮清楚它的應(yīng)用環(huán)境。等到程序出問題了才發(fā)現(xiàn),已經(jīng)晚了。
所以使用接口之前最好先了解一下,至少要看看官方文檔或者接口文檔/注釋。
哪怕是真的出問題了,看源碼也不失為一種排查問題的方式,因為代碼都是死的,它不會騙人。
代碼規(guī)約
阿里有這么一個代碼規(guī)約:不建議我們直接使用Executors類中的線程池,而是通過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同學(xué)需要更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風(fēng)險。
以前我還不太理解,心想使用Executors類可以提高可讀性,JDK提供了這樣的工具類,不用白不用。直到遇到這個問題,才明白這條規(guī)約的良苦用心。
如果我們使用規(guī)范的方式去使用線程池,而不是用一個所謂的Spring提供的“線程池”,就不會遇到這個問題了。
明確接口職責(zé)
再來想一想為什么同事會把它當(dāng)成一個線程池?因為它的類名、方法名都太像一個線程池了。它實現(xiàn)了Executor接口的execute方法,才導(dǎo)致我們誤以為它是一個線程池。
所以回歸到Executor這個接口上來,它的職責(zé)究竟是什么?我們可以在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. */大意就是,在將來某個時間執(zhí)行傳入的命令,這個命令可能會在一個新的線程里面執(zhí)行,可能會在線程池里,也可能在調(diào)用這個方法的線程中,具體怎么執(zhí)行是由實現(xiàn)類去決定的。
所以這才是Executor這個類的職責(zé),它的職責(zé)并不是提供一個線程池的接口,而是提供一個“將來執(zhí)行命令”的接口。
所以,真正能代表線程池意義的,是ThreadPoolExecutor類,而不是Executor接口。
在我們寫代碼的時候,也要定義清楚接口的職責(zé)喲。這樣別人用你的接口或者閱讀源碼的時候,才不會疑惑。
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
總結(jié)
以上是生活随笔為你收集整理的你也被Spring的这个“线程池”坑过吗?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Navicat for mysql导入.
- 下一篇: JS-打点计时器