專業在線打字練習網站-巧手打字通,只輸出有價值的知識。
一 前言
線程池作為初學者常感困惑的一個領域,本次“巧手打字通課堂”將深入剖析其中幾個最為普遍的誤區。為了更清晰地闡述這些知識點,讓我們以一個具體定義的線程池為例來展開説明。如下:
ThreadPoolExecutor executor = new ThreadPoolExecutor(20,50,100L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>(100));
二 線程池創建時機的誤解
- 問題:如果往線程池提交120個任務(假設提交的過程中沒有任務執行完成退出的情況),正常情況下會有多少個活躍線程,隊列裏有多少個任務?
解答這個問題的關鍵在於深入理解線程池底層的運作機制。具體而言,核心線程數、最大線程數以及它們與任務隊列之間的協同工作過程,可以通過參考下圖的詳細説明來獲得更清晰的認知:
- 建議:在處理前台流量密集的業務網關係統時,一個優化的策略是將核心線程數與最大線程數設置為相等值。這一舉措旨在避免當系統接近線程擴展的閾值時,因頻繁地創建和銷燬線程池而導致的服務響應波動,即所謂的“服務響應毛刺”。這種做法背後的邏輯與JVM中建議將-Xms(初始堆內存大小)和-Xmx(最大堆內存大小)參數配置為相等值相類似,都是為了減少因資源動態調整帶來的性能波動,確保系統穩定運行。
三 線程數越多越好嗎?
1. 線程的數量並非越多越好
具體原因可以歸結為以下幾點:
- 每個線程的創建都會消耗系統的內存資源。根據JVM規範,默認情況下,每個線程的棧大小被限制在約1MB(這一值可通過JVM啓動參數-Xss進行調整)。因此,當線程數量過多時,會顯著增加內存的消耗,影響系統資源的有效利用。
- 如果線程的創建與銷燬所需的時間總和超過了實際執行任務的時間,那麼創建額外的線程便顯得毫無意義,反而會增加系統的負擔。
- 過多的線程還可能導致操作系統頻繁地進行線程上下文切換,這不僅會增加CPU的開銷,還會減少CPU有效執行用户代碼的時間,從而對系統性能產生不利影響。
2. 那設置多少線程數合適呢?
根據Little's Law:一個系統請求數等於請求的到達率與平均每個單獨請求花費的時間之乘積。
系統平均請求數,估算公式如下:
線程池大小 = ((線程 IO time + 線程 CPU time )/線程 CPU time )* CPU數目
3. 舉個例子:
當服務器擁有8核CPU時,若一個任務線程的CPU執行時間為20毫秒,而線程因等待(如網絡IO、磁盤IO)所耗費的時間為80毫秒,理論上最佳線程數的計算方式為:(等待時間 + CPU時間) / CPU時間 CPU核數 = (80ms + 20ms) / 20ms 8 = 40。這意味着,在不考慮其他系統負載和資源競爭的情況下,設置40個線程可能達到最佳性能。然而,這一結論僅基於理論計算,實際部署時需根據系統具體表現進行調整。
值得注意的是,一個複雜的系統中往往部署了多個線程池,它們之間會爭奪CPU、網絡帶寬、內存等寶貴資源。因此,最佳線程數的設定還需綜合考慮系統整體的負載狀況、資源利用率以及各任務的實際執行特性,通過性能測試來驗證並優化。
四 線程池隊列長度設置多少合適?
不當的線程池隊列配置會引發嚴重後果,輕者導致任務執行延遲,用户無法及時獲取結果;重者則可能因內存耗盡而引發OOM(OutOfMemoryError)錯誤。為避免這些問題,以下是關於如何合理設置隊列長度的幾點建議:
- 明確指定隊列大小:避免使用默認的最大值(如Integer.MAX_VALUE),因為這可能導致無限制的內存佔用,最終引發內存溢出。明確設定一個合理的隊列長度限制是預防此類問題的關鍵。
- 基於實際場景調整隊列大小:對於無嚴格運行時間限制的任務,雖然可以設置較大的隊列以容納更多任務,但應同時考慮系統穩定性及異常情況下的任務保護,比如系統重啓可能導致任務丟失。因此,在增大隊列時,需權衡任務持久性與系統安全。
- 面向C端用户的任務需精細計算隊列大小:針對有嚴格響應時間要求的任務,如面向C端用户的服務,需根據任務執行速度和服務超時時間精確計算隊列容量。例如,若核心線程數為20,單任務執行時長為500ms,服務承諾的響應超時時長為2000ms,則隊列大小可計算為20*((2000/500)-1)=60。這樣既能確保在超時前任務有機會被處理,又避免了隊列過長導致的請求超時失效問題,從而保持服務響應的有效性和及時性。
五 丟棄策略也有坑
問題一:拒絕策略設置為DiscardPolicy或DiscardOldestPolicy與Future對象調用get()方法的阻塞問題
在Java的併發編程中,線程池(如ExecutorService)是一個強大的工具,用於管理一組併發執行的線程。然而,當線程池達到其最大容量時,新提交的任務需要被處理,這通常通過拒絕策略(RejectedExecutionHandler)來定義。DiscardPolicy和DiscardOldestPolicy是兩種常見的拒絕策略,它們分別代表直接丟棄新任務和丟棄隊列中最舊的任務,而不進行任何形式的通知或處理。
- 問題剖析
- DiscardPolicy:當線程池無法接受新任務時,此策略會靜默地丟棄新提交的任務,不拋出異常也不返回任何錯誤。這意味着,如果你依賴於任務的執行結果,並且沒有通過其他方式監控任務的提交狀態,你可能會丟失重要任務而不自知。
- DiscardOldestPolicy:與DiscardPolicy不同,這個策略會嘗試通過丟棄隊列中等待時間最長的任務來為新任務騰出空間。然而,同樣地,它也不會對任務提交者提供任何反饋,除非你有額外的機制來追蹤任務的執行狀態。
- Future對象的get()方法阻塞問題
當使用上述任一拒絕策略,並且存在被拒絕的任務時,如果你嘗試通過之前提交任務獲得的Future對象調用get()方法來獲取結果,可能會遇到線程被無限期阻塞的情況。這是因為get()方法會等待任務完成並返回其結果,但如果任務實際上從未被執行(因為被丟棄了),那麼調用線程就會一直等待,除非設置了超時時間。 -
設置超時時間:在調用Future.get()時,應該始終指定一個超時時間(如使用get(long timeout, TimeUnit unit)),以防止線程無限期等待。
try { Future<Result> future = executor.submit(task); Result result = future.get(10, TimeUnit.SECONDS); // 等待最多10秒 // 處理結果 } catch (TimeoutException e) { // 處理超時情況,可能是任務被拒絕或執行時間太長 } catch (InterruptedException | ExecutionException e) { // 處理其他可能的異常 } - 監控線程池狀態:定期監控線程池的狀態(如隊列大小、活躍線程數等),以便在必要時採取措施,如調整線程池大小或優化任務處理邏輯。
- 使用其他拒絕策略:如果任務丟失是不可接受的,可以考慮使用CallerRunsPolicy(在提交任務的線程中直接執行)或自定義的拒絕策略,這些策略可以提供更明確的反饋或處理邏輯。
問題二:Future對象未調用get()方法與任務異常的感知
當使用ExecutorService.submit()提交任務時,該方法會返回一個Future對象,該對象代表了異步計算的結果。然而,如果任務執行過程中拋出了異常,並且你沒有在任務內部捕獲這些異常,也沒有通過調用Future.get()方法來獲取結果,那麼這些異常信息在線程池外部是無法被感知到的。
1. 問題詳解:
- 異常丟失:如果任務中發生了異常且未被捕獲,這個異常將會被封裝在ExecutionException中,並在調用Future.get()時拋出。但是,如果get()方法從未被調用,那麼這個異常就會默默地丟失,導致你無法得知任務執行失敗的原因。
2. 建議與示例:
-
在任務中捕獲異常:在任務內部使用try-catch塊來捕獲並處理可能發生的異常。這可以通過打印日誌、發送告警等方式來實現,以便在任務失敗時能夠及時發現並處理。
Runnable task = () -> { try { // 執行任務邏輯 } catch (Exception e) { // 捕獲異常並處理 logger.error("任務執行失敗", e); } }; -
調用get()並指定超時時間:即使你在任務內部已經處理了異常,仍然建議調用Future.get(long timeout, TimeUnit unit)來獲取結果,並處理可能拋出的ExecutionException,以確保所有異常情況都能被妥善處理。
Future<Void> future = executor.submit(task); try { future.get(10, TimeUnit.SECONDS); // 等待任務完成,並處理可能拋出的異常 } catch (TimeoutException | InterruptedException | ExecutionException e) { // 處理異常 }
六 謹防多業務的線程池共享
多條業務線共用單一的線程池資源,潛藏着多重隱患:
- 難以兼顧各業務線的獨特需求,使得線程池的優化變得複雜而低效;
- 一旦某個業務的任務處理出現問題,其低下的效率或錯誤處理可能波及並影響其他業務線的任務執行效率與穩定性;
- 在問題排查階段,由於線程池共享,難以直接通過線程池名稱等常規手段迅速定位到具體業務線的問題所在。
因此,推薦採取線程池隔離策略,從設計之初就確保各條業務線的任務處理在獨立的線程池環境中進行,以此保障它們之間互不干擾,各自穩定運行。
七 其他潛在風險
- ThreadLocal與線程池結合使用時的信息錯亂:由於線程池中的線程會被複用,若這些線程內使用了ThreadLocal來存儲數據,那麼在線程被重新分配給不同任務時,可能會導致之前存儲的信息被錯誤地訪問或修改,進而引發數據錯亂的問題。
- 業務線中父子線程池嵌套導致的阻塞:在複雜的業務邏輯中,若存在父子線程池相互嵌套使用的情況,可能會因為子線程池的阻塞或異常而影響到父線程池的正常運行,甚至導致整個業務流程在單點處發生阻塞,影響系統整體的性能和穩定性。
- 線程池資源閒置與浪費:在某些業務場景下,線程池被初始化後卻並未得到充分利用,特別是在某些業務功能下線或調整時,這些閒置的線程池仍然佔用着系統資源,造成不必要的資源浪費。
- 實時創建線程池導致的效率低下:在請求處理過程中實時創建線程池,不僅無法發揮線程複用的優勢,還可能因為頻繁地創建和銷燬線程而增加系統的開銷。建議將線程池定義為靜態共享變量,在應用啓動時或初始化階段進行創建,以便在整個應用生命週期內複用,從而提高性能和資源利用率。