日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

你也被Spring的这个“线程池”坑过吗?

發布時間:2025/3/16 javascript 23 豆豆
生活随笔 收集整理的這篇文章主要介紹了 你也被Spring的这个“线程池”坑过吗? 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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

線程池提交方法執行一秒多?那不對啊,線程池提交應該是一個很快的操作,一般情況下不應該執行一秒多那么長的時間。

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

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

雖然執行的Job里面有一個for循環,可能比較耗時,但是execute提交任務的時候,并不會去真正去執行Job,所以應該不是這個原因引起的。


分析

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

線程池原理圖

然后逐個分析一下有可能耗時一秒多的操作:

創建線程耗時?

根據上面的圖,我們可以知道,如果核心線程數量設置過大,就可能會不斷創建新的核心線程去執行任務。同理,如果核心線程池和任務隊列都滿了,會創建非核心線程去執行任務。

創建線程是比較耗時的,而且Java線程池在這里創建線程的時候還上了鎖。

final?ReentrantLock?mainLock?=?this.mainLock; mainLock.lock();

我們寫個簡單的程序,可以模擬出來線程池耗時的操作,下面這段代碼創建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);

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

入任務隊列耗時?

線程池的任務隊列是一個同步隊列。所以入隊列操作是同步的。

常用的幾個同步隊列:

  • LinkedBlockingQueue

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

  • ArrayBlockingQueue

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

  • SynchronousQueue

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

  • DelayQueue

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

  • 所以使用特殊的同步隊列還是有可能導致execute方法阻塞一秒多的,比如SynchronousQueue。如果配合一個特殊的“拒絕策略”,是有可能造成這個現象的,我們將在下面給出例子。

    拒絕策略?

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

  • ThreadPoolExecutor.AbortPolicy:默認拒絕處理策略,丟棄任務并拋出異常。

  • ThreadPoolExecutor.DiscardPolicy:丟棄新來的任務,但是不拋出異常。

  • ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列頭部(最舊的)的任務,然后重新嘗試執行程序(如果再次失敗,重復此過程)。

  • 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();//?下面這段代碼,第三行會輸出1001System.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

    while循環去檢查,如果當前并發線程數量大于等于設置的最大值,就等待。

    找到原因了,這應該就是罪魁禍首??墒菫槭裁碨pring要這么設計呢?

    我們在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接口。

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

    有道無術,術可成;有術無道,止于術

    歡迎大家關注Java之道公眾號

    好文章,我在看??

    總結

    以上是生活随笔為你收集整理的你也被Spring的这个“线程池”坑过吗?的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。