自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

動(dòng)態(tài)調(diào)試線程池?這幾個(gè)坑讓我崩潰三天

開發(fā) 前端
現(xiàn)在再看線程池,雖然還是有點(diǎn)發(fā)怵,但至少知道從哪里入手去分析問(wèn)題了。踩坑不可怕,怕的是踩完坑還不知道坑從哪來(lái)。希望我的這些經(jīng)歷能讓你少走點(diǎn)彎路,下次遇到線程池問(wèn)題,能笑著說(shuō):"哦,這個(gè)坑我見過(guò)!"

兄弟們,凌晨項(xiàng)目里的核心接口響應(yīng)時(shí)間飆升到 5 秒以上,我強(qiáng)撐著睡意打開監(jiān)控平臺(tái),發(fā)現(xiàn)線程池的活躍線程數(shù)早就突破了預(yù)設(shè)的最大限制,隊(duì)列里還積壓著 thousands 級(jí)的任務(wù)。那一刻,我盯著屏幕上紅紅綠綠的曲線,恨不得把線程池的源碼拆開來(lái)揉碎了看 —— 這已經(jīng)是這周第三次因?yàn)榫€程池問(wèn)題被從床上拽起來(lái)了。

一、線程池配置的 "玄學(xué)" 陷阱

剛開始接手這個(gè)項(xiàng)目時(shí),我看著前輩留下的線程池配置陷入沉思:new FixedThreadPool(100),為什么是 100?問(wèn)了一圈才知道,哦,原來(lái)之前有個(gè)需求說(shuō)要支持 200 并發(fā),所以拍腦袋把核心線程數(shù)設(shè)成了 100。得,合著線程池配置全靠玄學(xué)。

1.1 被誤解的 "核心線程數(shù)"

很多人(包括當(dāng)初的我)都以為核心線程數(shù)就是 "最大能同時(shí)處理的任務(wù)數(shù)",其實(shí)這是個(gè)天大的誤會(huì)。舉個(gè)栗子,假設(shè)你開了家小餐館,核心線程數(shù)就像是店里固定雇傭的服務(wù)員,哪怕他們閑著沒(méi)事干(線程空閑),也不會(huì)被辭退(不會(huì)銷毀)。而最大線程數(shù)呢,相當(dāng)于你還能臨時(shí)招聘的兼職服務(wù)員,但這些兼職是要加錢的(創(chuàng)建線程有開銷),而且不是無(wú)限量供應(yīng)的(受限于系統(tǒng)資源)。

我曾經(jīng)在一個(gè)文件處理服務(wù)里用了newCachedThreadPool,想著 "自動(dòng)回收空閑線程,多智能啊"。結(jié)果在批量處理大文件時(shí),線程數(shù)直接飆升到幾百,系統(tǒng)差點(diǎn)被壓垮。后來(lái)才明白,這個(gè)線程池的核心線程數(shù)是 0,最大線程數(shù)是Integer.MAX_VALUE,簡(jiǎn)直就是個(gè) "線程黑洞",遇到突發(fā)流量直接失控。

正確的打開方式應(yīng)該是根據(jù)任務(wù)類型來(lái)配置:CPU 密集型任務(wù),核心線程數(shù)可以設(shè)為CPU核心數(shù)+1;IO 密集型任務(wù),因?yàn)榫€程大部分時(shí)間在等待 IO 操作,核心線程數(shù)可以適當(dāng)多一些,比如2*CPU核心數(shù)。但這些也只是經(jīng)驗(yàn)值,實(shí)際還得結(jié)合監(jiān)控來(lái)調(diào)整。

1.2 隊(duì)列選擇的 "隱形炸彈"

線程池里的隊(duì)列就像是餐館里的候餐區(qū),不同的隊(duì)列有不同的 "接待能力"。有一次我用了SynchronousQueue,以為它能 "高效處理同步任務(wù)",結(jié)果發(fā)現(xiàn)任務(wù)稍微多一點(diǎn),就直接觸發(fā)拒絕策略了。為啥?因?yàn)檫@個(gè)隊(duì)列根本不存儲(chǔ)任務(wù),每個(gè)任務(wù)都得馬上找線程處理,找不到就拒絕,簡(jiǎn)直就是個(gè) "急性子隊(duì)列"。

而ArrayBlockingQueue呢,就像個(gè)容量固定的候餐區(qū),任務(wù)來(lái)了先在里面排隊(duì),排滿了再考慮創(chuàng)建新的線程(直到最大線程數(shù))。如果隊(duì)列選得不合適,比如用了無(wú)界隊(duì)列LinkedBlockingQueue,還把最大線程數(shù)設(shè)得很小,那就相當(dāng)于候餐區(qū)無(wú)限大,服務(wù)員(線程)卻很少,任務(wù)越積越多,最后系統(tǒng)內(nèi)存被耗盡,直接 OOM。

我見過(guò)最離譜的配置是:核心線程數(shù) 10,最大線程數(shù) 20,隊(duì)列容量 1000。結(jié)果遇到突發(fā)流量時(shí),線程數(shù)一直停留在 10,隊(duì)列里的任務(wù)瘋狂增長(zhǎng),直到把內(nèi)存撐爆。原因就是沒(méi)有正確理解隊(duì)列和線程數(shù)之間的關(guān)系,以為 "隊(duì)列能緩沖一切",卻忽略了線程數(shù)不足時(shí),隊(duì)列積壓會(huì)越來(lái)越嚴(yán)重。

二、任務(wù)里的 "定時(shí)炸彈"

線程池本身配置正確了,就能高枕無(wú)憂了嗎?太天真了!任務(wù)里藏著的各種 "幺蛾子",分分鐘讓線程池陷入困境。

2.1 阻塞任務(wù)的 "溫柔一刀"

在一個(gè)電商訂單處理系統(tǒng)里,我們?cè)诰€程池的任務(wù)里調(diào)用了一個(gè)第三方接口,本來(lái)以為響應(yīng)時(shí)間最多 200ms,結(jié)果某天第三方接口突然抽風(fēng),響應(yīng)時(shí)間變成了 5 秒。而我們的任務(wù)沒(méi)有設(shè)置超時(shí)時(shí)間,導(dǎo)致線程一直卡在那里等待,核心線程全部被阻塞,新的任務(wù)只能排隊(duì),隊(duì)列很快就滿了,然后開始創(chuàng)建最大線程數(shù)的線程,直到所有線程都被阻塞,整個(gè)線程池徹底癱瘓。

這就好比餐館里的服務(wù)員都去處理一個(gè)超級(jí)麻煩的顧客,半天回不來(lái),后面的顧客只能一直等著,最后門口排起了長(zhǎng)隊(duì)。解決辦法很簡(jiǎn)單:給每個(gè)外部調(diào)用設(shè)置超時(shí)時(shí)間,比如用Future.get(timeout, TimeUnit),或者用 Hystrix 做熔斷降級(jí)。但說(shuō)起來(lái)簡(jiǎn)單,實(shí)際開發(fā)中很多人(包括我)都會(huì)忘記給任務(wù)設(shè)置超時(shí),覺(jué)得 "正常情況下不會(huì)有問(wèn)題",結(jié)果就被異常情況教做人。

2.2 異常處理的 "黑洞陷阱"

線程池里的任務(wù)如果拋出未捕獲的異常,默認(rèn)情況下線程會(huì)被銷毀。如果任務(wù)里有個(gè)隱藏的 NullPointerException,而且沒(méi)有做 try - catch,那就麻煩了。我曾經(jīng)遇到過(guò)一個(gè)線程池,核心線程數(shù) 5,結(jié)果每天早上都會(huì)發(fā)現(xiàn)只剩下 2 個(gè)線程在工作。追查之后發(fā)現(xiàn),有個(gè)任務(wù)在特定數(shù)據(jù)下會(huì)拋出異常,而我們沒(méi)有處理,導(dǎo)致線程不斷被銷毀,又因?yàn)槭呛诵木€程,線程池會(huì)不斷創(chuàng)建新的線程來(lái)補(bǔ)充,但創(chuàng)建線程是有開銷的,而且在創(chuàng)建過(guò)程中,系統(tǒng)性能會(huì)受到影響。

更坑的是,如果你用了execute方法提交任務(wù),異常不會(huì)向上拋出,而是被線程池內(nèi)部的UncaughtExceptionHandler處理,默認(rèn)情況下只是打印日志。所以很多時(shí)候,你根本不知道任務(wù)里有異常,還以為是線程池自己 "鬧脾氣"。正確的做法是在任務(wù)里做好異常處理,或者給線程池設(shè)置自定義的UncaughtExceptionHandler,方便排查問(wèn)題。

三、拒絕策略的 "最后防線"

當(dāng)線程池的任務(wù)隊(duì)列滿了,而且線程數(shù)達(dá)到了最大線程數(shù),這時(shí)候就會(huì)觸發(fā)拒絕策略。別以為拒絕策略只是 "報(bào)錯(cuò)" 這么簡(jiǎn)單,里面的坑也不少。

3.1 默認(rèn)策略的 "冷暴力"

ThreadPoolExecutor有四種拒絕策略,默認(rèn)的是AbortPolicy,也就是直接拋出RejectedExecutionException。但如果你用execute方法提交任務(wù),這個(gè)異常不會(huì)被拋出,而是會(huì)被線程池的UncaughtExceptionHandler處理,這就導(dǎo)致你可能根本察覺(jué)不到任務(wù)被拒絕了。我就曾經(jīng)遇到過(guò)這種情況,線上系統(tǒng)默默拒絕了很多任務(wù),而我們的監(jiān)控卻沒(méi)有報(bào)警,直到用戶投訴才發(fā)現(xiàn)問(wèn)題。

另一種策略是CallerRunsPolicy,它會(huì)讓提交任務(wù)的線程來(lái)執(zhí)行任務(wù)。這在某些場(chǎng)景下很有用,比如主線程可以幫忙處理一些任務(wù),減輕線程池的壓力。但如果主線程是個(gè) UI 線程,那就麻煩了,任務(wù)在 UI 線程里執(zhí)行,會(huì)導(dǎo)致界面卡頓,用戶體驗(yàn)極差。我曾經(jīng)在一個(gè)桌面應(yīng)用里誤用了這個(gè)策略,結(jié)果用戶反饋點(diǎn)擊按鈕后界面卡死,就是因?yàn)槿蝿?wù)在 UI 線程里執(zhí)行,阻塞了事件循環(huán)。

3.2 自定義拒絕策略的 "翻車現(xiàn)場(chǎng)"

為了應(yīng)對(duì)特定場(chǎng)景,我們自定義了一個(gè)拒絕策略,把被拒絕的任務(wù)寫入數(shù)據(jù)庫(kù),等后續(xù)再處理。想法很美好,現(xiàn)實(shí)很骨感。在高并發(fā)情況下,寫入數(shù)據(jù)庫(kù)的操作本身也會(huì)成為瓶頸,導(dǎo)致拒絕策略的執(zhí)行時(shí)間很長(zhǎng),反而加劇了系統(tǒng)的負(fù)載。而且,如果數(shù)據(jù)庫(kù)連接出現(xiàn)問(wèn)題,拒絕策略里的代碼也會(huì)拋出異常,導(dǎo)致線程池徹底無(wú)法處理任務(wù)。

這就提醒我們,自定義拒絕策略時(shí)一定要考慮拒絕策略本身的可靠性和性能,不能在拒絕策略里做太復(fù)雜的操作,更不能引入新的瓶頸。最好是做一個(gè)簡(jiǎn)單的日志記錄,或者把任務(wù)放入一個(gè)可靠的消息隊(duì)列,等系統(tǒng)恢復(fù)后再處理。

四、線程泄漏的 "慢性毒藥"

線程泄漏不像前面那些問(wèn)題,一來(lái)就是 "狂風(fēng)暴雨",它更像是一種 "慢性毒藥",慢慢侵蝕系統(tǒng)的性能。

4.1 被遺忘的 "空閑線程"

線程池里的線程是會(huì)重復(fù)利用的,如果任務(wù)里有一些靜態(tài)變量或者上下文沒(méi)有清理,就會(huì)導(dǎo)致線程持有這些資源不釋放,形成泄漏。我曾經(jīng)在一個(gè)定時(shí)任務(wù)線程池里發(fā)現(xiàn),隨著時(shí)間的推移,線程的內(nèi)存占用越來(lái)越高。追查發(fā)現(xiàn),任務(wù)里用了一個(gè)靜態(tài)的緩存 map,每次任務(wù)執(zhí)行都會(huì)往里面添加數(shù)據(jù),卻沒(méi)有清理,導(dǎo)致緩存無(wú)限增長(zhǎng),而線程又不會(huì)被銷毀,所以內(nèi)存就一直被占用。

還有一種情況是,任務(wù)里調(diào)用了外部資源,比如數(shù)據(jù)庫(kù)連接、文件流等,但沒(méi)有正確關(guān)閉。雖然線程會(huì)被放回線程池,但這些外部資源沒(méi)有釋放,隨著線程的重復(fù)使用,資源泄漏越來(lái)越嚴(yán)重。這就要求我們?cè)谌蝿?wù)里一定要養(yǎng)成良好的習(xí)慣,使用try - finally來(lái)關(guān)閉資源,不管任務(wù)是否成功,都要確保資源被正確釋放。

4.2 線程池未正確關(guān)閉的 "僵尸線程"

在程序關(guān)閉時(shí),如果沒(méi)有正確關(guān)閉線程池,里面的線程會(huì)一直運(yùn)行,成為 "僵尸線程"。我在一次系統(tǒng)升級(jí)后發(fā)現(xiàn),舊版本的線程池沒(méi)有被關(guān)閉,新版本又創(chuàng)建了新的線程池,導(dǎo)致系統(tǒng)里存在大量空閑線程,浪費(fèi)了很多資源。正確的做法是在程序退出前,調(diào)用shutdown或者shutdownNow方法來(lái)關(guān)閉線程池,等待任務(wù)執(zhí)行完畢或者中斷正在執(zhí)行的任務(wù)。

五、動(dòng)態(tài)調(diào)整參數(shù)的 "作死操作"

想著線程池參數(shù)可以動(dòng)態(tài)調(diào)整,以為這樣就能靈活應(yīng)對(duì)各種流量場(chǎng)景,結(jié)果卻踩了大坑。

5.1 并發(fā)調(diào)整的 "線程安全" 問(wèn)題

我們通過(guò)配置中心動(dòng)態(tài)修改線程池的核心線程數(shù)和最大線程數(shù),本來(lái)以為很簡(jiǎn)單,結(jié)果在高并發(fā)情況下,出現(xiàn)了各種奇怪的問(wèn)題。比如,當(dāng)多個(gè)線程同時(shí)修改核心線程數(shù)時(shí),導(dǎo)致線程池的狀態(tài)混亂,有的線程不知道該創(chuàng)建新線程還是銷毀舊線程。原來(lái),ThreadPoolExecutor的參數(shù)調(diào)整方法雖然是線程安全的,但如果在調(diào)整的同時(shí),線程池正在處理大量任務(wù),還是可能會(huì)出現(xiàn)一些不可預(yù)知的問(wèn)題。

正確的做法是,在調(diào)整參數(shù)時(shí),盡量選擇在系統(tǒng)低負(fù)載的時(shí)候進(jìn)行,或者對(duì)調(diào)整操作進(jìn)行加鎖,確保同一時(shí)間只有一個(gè)線程在修改參數(shù)。不過(guò),這又會(huì)引入新的并發(fā)控制問(wèn)題,增加了系統(tǒng)的復(fù)雜度。

5.2 過(guò)度調(diào)整的 "震蕩效應(yīng)"

為了讓線程池始終保持最佳狀態(tài),我們寫了一個(gè)自動(dòng)調(diào)整參數(shù)的腳本,根據(jù)實(shí)時(shí)的任務(wù)隊(duì)列長(zhǎng)度和線程活躍數(shù)來(lái)動(dòng)態(tài)調(diào)整核心線程數(shù)。結(jié)果這個(gè)腳本反而成了災(zāi)難,參數(shù)一會(huì)兒調(diào)大,一會(huì)兒調(diào)小,線程池里的線程頻繁創(chuàng)建和銷毀,系統(tǒng)的上下文切換開銷急劇增加,性能反而比固定參數(shù)時(shí)還要差。

這就像開車時(shí)頻繁換擋,反而會(huì)讓車子行駛不平穩(wěn)。線程池的參數(shù)調(diào)整需要有一定的滯后性,不能根據(jù)即時(shí)的監(jiān)控?cái)?shù)據(jù)就馬上調(diào)整,要給系統(tǒng)一個(gè)穩(wěn)定的時(shí)間窗口。而且,自動(dòng)調(diào)整參數(shù)是一個(gè)非常復(fù)雜的過(guò)程,需要結(jié)合大量的業(yè)務(wù)場(chǎng)景和性能數(shù)據(jù),不是隨便寫個(gè)腳本就能搞定的。

六、調(diào)試工具的 "救命稻草"

說(shuō)了這么多坑,那怎么才能快速定位問(wèn)題呢?還好有這些調(diào)試工具,讓我在崩潰的邊緣找到了救命稻草。

6.1 jstack:線程快照的 "X 光機(jī)"

當(dāng)發(fā)現(xiàn)線程池有問(wèn)題時(shí),首先可以用jstack <pid>命令獲取線程快照。通過(guò)分析線程快照,你可以看到每個(gè)線程的狀態(tài),是在運(yùn)行、阻塞、等待,還是在鎖競(jìng)爭(zhēng)。比如,我曾經(jīng)通過(guò) jstack 發(fā)現(xiàn)大量線程處于BLOCKED狀態(tài),指向同一個(gè)數(shù)據(jù)庫(kù)連接的獲取操作,很快就定位到是數(shù)據(jù)庫(kù)連接池耗盡的問(wèn)題。

看線程快照時(shí),重點(diǎn)關(guān)注WAITING和BLOCKED狀態(tài)的線程,尤其是那些數(shù)量較多的線程,它們背后往往隱藏著問(wèn)題。同時(shí),注意線程的調(diào)用棧,能直接告訴你線程卡在哪個(gè)方法里。

6.2 VisualVM:圖形化的 "監(jiān)控大屏"

VisualVM 是個(gè)非常強(qiáng)大的工具,它可以實(shí)時(shí)監(jiān)控線程池的各種指標(biāo),比如活躍線程數(shù)、任務(wù)隊(duì)列長(zhǎng)度、線程創(chuàng)建和銷毀次數(shù)等。我經(jīng)常用它來(lái)觀察線程池在不同負(fù)載下的表現(xiàn),比如模擬突發(fā)流量時(shí),看看核心線程數(shù)和最大線程數(shù)是否合理,隊(duì)列是否會(huì)積壓任務(wù)。

而且,VisualVM 還支持插件擴(kuò)展,比如安裝 VisualGC 插件,可以實(shí)時(shí)查看 JVM 的垃圾回收情況,結(jié)合線程池的指標(biāo),能更全面地分析系統(tǒng)性能問(wèn)題。

6.3 Arthas:動(dòng)態(tài)調(diào)試的 "瑞士軍刀"

Arthas 簡(jiǎn)直就是線上調(diào)試的神器,它可以在不重啟應(yīng)用的情況下,動(dòng)態(tài)查看線程池的狀態(tài),甚至修改線程池的參數(shù)。比如,我可以用thread命令查看當(dāng)前活躍的線程,用sc -d java.util.concurrent.ThreadPoolExecutor查看線程池的詳細(xì)信息,包括核心線程數(shù)、最大線程數(shù)、隊(duì)列容量、活躍線程數(shù)等。

更厲害的是,Arthas 還可以監(jiān)控方法的調(diào)用情況,比如用watch命令監(jiān)控線程池的execute方法,看看每次提交的任務(wù)是什么,有沒(méi)有異常拋出。有了 Arthas,很多問(wèn)題都能在第一時(shí)間定位,不用再靠猜了。

七、總結(jié):踩坑后的 "真香" 經(jīng)驗(yàn)

這三天的崩潰經(jīng)歷,讓我對(duì)線程池有了全新的認(rèn)識(shí)。線程池就像家里的電器,看起來(lái)簡(jiǎn)單,用起來(lái)卻需要懂點(diǎn) "脾氣"??偨Y(jié)幾點(diǎn)經(jīng)驗(yàn):

  1. 拒絕 "拍腦袋" 配置:線程池的參數(shù)不是隨便設(shè)的,要根據(jù)業(yè)務(wù)場(chǎng)景、任務(wù)類型、系統(tǒng)資源來(lái)綜合考慮,最好先做壓測(cè),再結(jié)合監(jiān)控調(diào)整。
  2. 任務(wù)里的 "坑" 更可怕:永遠(yuǎn)不要相信任務(wù)是 "完美" 的,做好異常處理、設(shè)置超時(shí)時(shí)間、釋放資源,這些細(xì)節(jié)決定了線程池能否穩(wěn)定運(yùn)行。
  3. 拒絕策略不是 "擺設(shè)":根據(jù)業(yè)務(wù)需求選擇合適的拒絕策略,自定義拒絕策略時(shí)要考慮可靠性和性能,別讓最后防線變成新的問(wèn)題源。
  4. 動(dòng)態(tài)調(diào)整要 "謹(jǐn)慎":除非你真的很清楚自己在做什么,否則不要輕易動(dòng)態(tài)調(diào)整線程池參數(shù),固定參數(shù)在很多場(chǎng)景下反而更穩(wěn)定。
  5. 用好調(diào)試工具:jstack、VisualVM、Arthas 這些工具,能讓你快速定位問(wèn)題,節(jié)省大量時(shí)間,平時(shí)一定要多學(xué)習(xí)它們的用法。

現(xiàn)在再看線程池,雖然還是有點(diǎn)發(fā)怵,但至少知道從哪里入手去分析問(wèn)題了。踩坑不可怕,怕的是踩完坑還不知道坑從哪來(lái)。希望我的這些經(jīng)歷能讓你少走點(diǎn)彎路,下次遇到線程池問(wèn)題,能笑著說(shuō):"哦,這個(gè)坑我見過(guò)!"

責(zé)任編輯:武曉燕 來(lái)源: 石杉的架構(gòu)筆記
相關(guān)推薦

2020-01-30 17:58:56

GitHub代碼開發(fā)者

2020-02-03 09:29:32

JavaScript代碼斷點(diǎn)

2020-04-29 14:10:44

Java線程池編程語(yǔ)言

2024-12-13 08:21:04

2024-12-10 00:00:25

2020-05-22 08:11:48

線程池JVM面試

2019-06-13 16:30:37

代碼Java編程語(yǔ)言

2023-01-29 08:04:24

線程池非核心線程任務(wù)

2025-04-09 08:25:00

JavaScript數(shù)組解構(gòu)賦值

2019-09-25 10:37:16

SpringBeanUtils接口

2022-08-16 08:27:20

線程毀線程異步

2025-01-09 11:24:59

線程池美團(tuán)動(dòng)態(tài)配置中心

2022-03-23 07:54:05

Java線程池系統(tǒng)

2021-02-23 18:38:11

iPhone地圖蘋果

2020-04-20 14:50:02

前端技巧優(yōu)化

2022-03-14 08:02:08

輕量級(jí)動(dòng)態(tài)線程池

2015-03-24 16:29:55

默認(rèn)線程池java

2024-09-13 09:06:22

2024-02-04 08:26:38

線程池參數(shù)內(nèi)存

2019-09-09 16:30:42

Redis架構(gòu)數(shù)據(jù)庫(kù)
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)