【踩坑指南】線程池使用不當(dāng)?shù)奈鍌€坑
線程池是 Java 多線程編程中的一個重要概念,它可以有效地管理和復(fù)用線程資源,提高系統(tǒng)的性能和穩(wěn)定性。但是線程池的使用也有一些注意事項和常見的錯誤,如果不小心,就可能會導(dǎo)致一些嚴(yán)重的問題,比如內(nèi)存泄漏、死鎖、性能下降等。最后文末還有免費紅包封面可以領(lǐng)取,回饋給各位讀者朋友。
本文將介紹線程池使用不當(dāng)?shù)奈鍌€坑,以及如何避免和解決它們,大綱如下:

坑一:線程池中異常消失
線程池執(zhí)行方法時要添加異常處理,這是一個老生常談的問題,可是直到最近我都有同事還在犯這個錯誤,所以我還是要講一下,不過我還提到了一種優(yōu)雅的線程池全局異常處理的方法,大家可以往下看。
問題原因
@Test
public void test() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5, 
        10, 
        60,
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<>(100000));
    Future<Integer> submit = threadPoolExecutor.execute(() -> {
        int i = 1 / 0; // 發(fā)生異常
        return i;
    });
}如上代碼,在線程池執(zhí)行任務(wù)時,沒有添加異常處理。導(dǎo)致任務(wù)內(nèi)部發(fā)生異常時,內(nèi)部錯誤無法被記錄下來。
解決方法
在線程池執(zhí)行任務(wù)方法內(nèi)添加 try/catch 處理,代碼如下,
@Test
public void test() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5, 
        10, 
        60,
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<>(100000));
    Future<Integer> submit = threadPoolExecutor.execute(() -> {
        try {
            int i = 1 / 0;
            return i;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
    });
}優(yōu)雅的進行線程池異常處理
當(dāng)線程池調(diào)用任務(wù)方法很多時,那么每個線程池任務(wù)執(zhí)行的方法內(nèi)都要添加 try/catch 處理,這就不優(yōu)雅了,其實 ThreadPoolExecutor 線程池類支持傳入 ThreadFactory 參數(shù)用于自定義線程工廠,這樣我們在創(chuàng)建線程時,就可以指定 setUncaughtExceptionHandler 異常處理方法。
這樣就可以做到全局處理異常了,代碼如下,
ThreadFactory threadFactory = r -> {
    Thread thread = new Thread(r);
    thread.setUncaughtExceptionHandler((t, e) -> {
        // 記錄線程異常
        log.error(e.getMessage(), e);
    });
    return thread;
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
    5, 
    10, 
    60,
    TimeUnit.SECONDS, 
    new ArrayBlockingQueue<>(100000));
threadPoolExecutor.execute(() -> {
    log.info("---------------------");
    int i = 1 / 0;
});不過要注意的是上面 setUncaughtExceptionHandler 方法只能針對線程池的 execute 方法來全局處理異常。對于線程池的 submit 方法是無法處理的。
坑二:拒絕策略設(shè)置錯誤導(dǎo)致接口超時
在 Java 中,線程池拒絕策略可以說一個常見八股文問題。大家雖然都記住了線程池有四種決絕策略,可是實際代碼編寫中,我發(fā)現(xiàn)大多數(shù)人都只會用 CallerRunsPolicy 策略(由調(diào)用線程處理任務(wù))。我吃過這個虧,因此也拿出來講講。
問題原因
曾經(jīng)有一個線上業(yè)務(wù)接口使用了線程池進行第三方接口調(diào)用,線程池配置里的拒絕策略采用的是 CallerRunsPolicy。示例代碼如下,
// 某個線上線程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        50, // 最小核心線程數(shù)
        50, // 最大線程數(shù),當(dāng)隊列滿時,能創(chuàng)建的最大線程數(shù)
        60L, TimeUnit.SECONDS, // 空閑線程超過核心線程時,回收該線程的最大等待時間
        new LinkedBlockingQueue<>(5000), // 阻塞隊列大小,當(dāng)核心線程使用滿時,新的線程會放進隊列
        new CustomizableThreadFactory("task"), // 自定義線程名
        new ThreadPoolExecutor.CallerRunsPolicy() // 線程執(zhí)行的拒絕策略
);
threadPoolExecutor.execute(() -> {
    // 調(diào)用第三方接口
    ...
});在第三方接口異常的情況下,線程池任務(wù)調(diào)用第三方接口一直超時,導(dǎo)致核心線程數(shù)、最大線程數(shù)堆積被占滿、阻塞隊列也被占滿的情況下,也就會執(zhí)行拒絕策略,但是由于使用的是 CallerRunsPolicy 策略,導(dǎo)致線程任務(wù)直接由我們的業(yè)務(wù)線程來執(zhí)行。
因為第三方接口異常,所以業(yè)務(wù)線程執(zhí)行也會繼繼續(xù)超時,線上服務(wù)采用的 Tomcat 容器,最終也就導(dǎo)致 Tomcat 的最大線程數(shù)也被占滿,進而無法繼續(xù)向外提供服務(wù)。
解決方法
首先我們要考慮業(yè)務(wù)接口的可用性,就算線程池任務(wù)被丟棄,也不應(yīng)該影響業(yè)務(wù)接口。
在業(yè)務(wù)接口穩(wěn)定性得到保證的情況下,在考慮到線程池任務(wù)的重要性,不是很重要的話,可以使用 DiscardPolicy 策略直接丟棄,要是很重要,可以考慮使用消息隊列來替換線程池。
坑三:重復(fù)創(chuàng)建線程池導(dǎo)致內(nèi)存溢出
不知道大家有沒有犯過這個問題,不過我確實犯過,歸根結(jié)底還是寫代碼前,沒有思考好業(yè)務(wù)邏輯,直接動手,寫一步算一步 ??。所以說寫代碼的前的一些邏輯梳理、拆分、代碼設(shè)計很重要。
問題原因
這個問題的原因很簡單,就是在一個方法內(nèi)重復(fù)創(chuàng)建了線程池,在執(zhí)行完之后卻沒有關(guān)閉。比較經(jīng)典的就是在定時任務(wù)內(nèi)使用線程池時有可能犯這個問題,示例代碼如下,
@XxlJob("test")
public void test() throws Exception {
    // 某個線上線程池配置如下
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            50, // 最小核心線程數(shù)
            50, // 最大線程數(shù),當(dāng)隊列滿時,能創(chuàng)建的最大線程數(shù)
            60L, TimeUnit.SECONDS, // 空閑線程超過核心線程時,回收該線程的最大等待時間
            new LinkedBlockingQueue<>(5000), // 阻塞隊列大小,當(dāng)核心線程使用滿時,新的線程會放進隊列
            new CustomizableThreadFactory("task"), // 自定義線程名
            new ThreadPoolExecutor.CallerRunsPolicy() // 線程執(zhí)行的拒絕策略
    );
    threadPoolExecutor.execute(() -> {
        // 任務(wù)邏輯
        ...
    });
}當(dāng)我們在定時任務(wù)中想使用線程池來縮短任務(wù)執(zhí)行時間時,千萬要注意別再任務(wù)內(nèi)創(chuàng)建了線程池,一旦犯了,基本都會在程序運行一段時間后發(fā)現(xiàn)程序突然間就掛了,留下了一堆內(nèi)存 dump 報錯的文件 ??。
解決方法
使用線程池單例,切勿重復(fù)創(chuàng)建線程池。示例代碼如下,
// 某個線上線程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        50, // 最小核心線程數(shù)
        50, // 最大線程數(shù),當(dāng)隊列滿時,能創(chuàng)建的最大線程數(shù)
        60L, TimeUnit.SECONDS, // 空閑線程超過核心線程時,回收該線程的最大等待時間
        new LinkedBlockingQueue<>(5000), // 阻塞隊列大小,當(dāng)核心線程使用滿時,新的線程會放進隊列
        new CustomizableThreadFactory("task"), // 自定義線程名
        new ThreadPoolExecutor.CallerRunsPolicy() // 線程執(zhí)行的拒絕策略
);
@XxlJob("test")
public void test() throws Exception {
    threadPoolExecutor.execute(() -> {
        // 任務(wù)邏輯
        // ...
    });
}坑四:共用線程池執(zhí)行不同類型任務(wù)導(dǎo)致效率低下
有時候,我們可能會想要節(jié)省線程資源,把不同類型的任務(wù)都放到同一個線程池中執(zhí)行,比如主要的業(yè)務(wù)邏輯和次要的日志記錄、監(jiān)控等。這看起來很合理,但是實際上,這樣做可能會導(dǎo)致一個任務(wù)影響另一個任務(wù),甚至導(dǎo)致死鎖的問題。
問題原因
問題的原因是,不同類型的任務(wù)可能有不同的執(zhí)行時間、優(yōu)先級、依賴關(guān)系等,如果放到同一個線程池中,就可能會出現(xiàn)以下幾種情況:
- 如果一個任務(wù)執(zhí)行時間過長,或者出現(xiàn)異常,那么它就會占用線程池中的一個線程,導(dǎo)致其他任務(wù)無法及時得到執(zhí)行,影響系統(tǒng)的吞吐量和響應(yīng)時間。
 - 如果一個任務(wù)的優(yōu)先級較低,或者不是很重要,那么它就可能搶占線程池中的一個線程,導(dǎo)致其他任務(wù)無法及時得到執(zhí)行,影響系統(tǒng)的可用性和正確性。
 - 如果一個任務(wù)依賴于另一個任務(wù)的結(jié)果,或者需要等待另一個任務(wù)的完成,那么它就可能造成線程池中的一個線程被阻塞,導(dǎo)致其他任務(wù)無法及時得到執(zhí)行,甚至導(dǎo)致死鎖的問題。
 
解決方法
解決方法也很簡單,就是使用不同的線程池來執(zhí)行不同類型的任務(wù),根據(jù)任務(wù)的特點和重要性來分配線程資源,避免一個任務(wù)影響另一個任務(wù)。具體來說,有以下幾個建議:
- 對于主要的業(yè)務(wù)邏輯,使用一個專門的線程池,根據(jù)業(yè)務(wù)的并發(fā)度和響應(yīng)時間,設(shè)置合適的線程池參數(shù),保證業(yè)務(wù)的正常運行和高效處理。
 - 對于次要的日志記錄、監(jiān)控等,使用一個單獨的線程池,根據(jù)任務(wù)的頻率和重要性,設(shè)置合適的線程池參數(shù),保證任務(wù)的異步執(zhí)行和不影響主業(yè)務(wù)。
 - 對于有依賴關(guān)系的任務(wù),使用一個單獨的線程池,根據(jù)任務(wù)的數(shù)量和復(fù)雜度,設(shè)置合適的線程池參數(shù),保證任務(wù)的有序執(zhí)行和不造成死鎖。
 
坑五:使用 ThreadLocal 和線程池的不兼容問題
ThreadLocal 是 Java 提供的一個工具類,它可以讓每個線程擁有自己的變量副本,從而實現(xiàn)線程間的數(shù)據(jù)隔離,比如存儲一些線程相關(guān)的上下文信息,如用戶 ID、請求 ID 等。這看起來很有用,但是如果和線程池一起使用,就可能會出現(xiàn)一些意想不到的問題,比如數(shù)據(jù)錯亂、內(nèi)存泄漏等。
問題原因
問題的原因是,ThreadLocal 和線程池的設(shè)計理念是相悖的,ThreadLocal 是基于線程的,而線程池是基于任務(wù)的。具體來說,有以下幾個問題:
- ThreadLocal 的變量是綁定在線程上的,而線程池的線程是可以復(fù)用的,如果一個線程執(zhí)行完一個任務(wù)后,沒有清理 ThreadLocal 的變量,那么這個變量就會被下一個執(zhí)行的任務(wù)繼承,導(dǎo)致數(shù)據(jù)錯亂的問題。
 - ThreadLocal 的變量是存儲在 Thread 類的一個 ThreadLocalMap 類型的屬性中的,這個屬性是一個弱引用的 Map,它的鍵是 ThreadLocal 對象,而值是變量的副本。如果 ThreadLocal 對象被回收,那么它的鍵就會失效,但是值還會保留在 Map 中,導(dǎo)致內(nèi)存泄漏的問題。
 
解決方法
解決方法也很簡單,就是在使用 ThreadLocal 和線程池的時候,注意以下幾點:
- 在使用 ThreadLocal 的變量之前,要確保為每個線程設(shè)置了正確的初始值,避免使用上一個任務(wù)的遺留值。
 - 在使用 ThreadLocal 的變量之后,要及時地清理 ThreadLocal 的變量,避免變量的副本被下一個執(zhí)行的任務(wù)繼承,或者占用內(nèi)存空間,導(dǎo)致內(nèi)存泄漏的問題??梢允褂?try-finally 語句,或者使用 Java 8 提供的 AutoCloseable 接口,來實現(xiàn)自動清理的功能。
 - 在使用 ThreadLocal 的時候,要注意線程池的大小和任務(wù)的數(shù)量,避免創(chuàng)建過多的 ThreadLocal 對象和變量的副本,導(dǎo)致內(nèi)存占用過大的問題??梢允褂靡恍┕ぞ撸?VisualVM,來監(jiān)控線程池和 ThreadLocal 的狀態(tài),及時發(fā)現(xiàn)和解決問題。
 
總結(jié)
本文給大家介紹了線程池使用不當(dāng)?shù)奈鍌€坑,分別是線程池中異常消失、線程池決絕策略設(shè)置錯誤、重復(fù)創(chuàng)建線程池導(dǎo)致內(nèi)存溢出、使用同一個線程池執(zhí)行不同類型的任務(wù)、使用 ThreadLocal 和線程池的不兼容問題,以及它們的問題原因和解決方法。希望這些內(nèi)容對大家有幫助。















 
 
 











 
 
 
 