8 個線程池最佳實踐和坑!使用不當直接生產(chǎn)事故!
共 13062字,需瀏覽 27分鐘
·
2024-07-26 07:32
閱讀本文大概需要 10 分鐘。
來自:網(wǎng)絡,侵刪
1、正確聲明線程池
ThreadPoolExecutor 的構造函數(shù)來聲明,避免使用Executors 類創(chuàng)建線程池,會有 OOM 風險。
Executors 返回線程池對象的弊端如下(后文會詳細介紹到):
-
FixedThreadPool和SingleThreadExecutor:使用的是無界的LinkedBlockingQueue,任務隊列最大長度為Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。 -
CachedThreadPool:使用的是同步隊列SynchronousQueue, 允許創(chuàng)建的線程數(shù)量為Integer.MAX_VALUE,可能會創(chuàng)建大量線程,從而導致 OOM。 -
ScheduledThreadPool和SingleThreadScheduledExecutor: 使用的無界的延遲阻塞隊列DelayedWorkQueue,任務隊列最大長度為Integer.MAX_VALUE,可能堆積大量的請求,從而導致 OOM。
Executors提供的兩種快捷的線程池的原因還有:
-
實際使用中需要根據(jù)自己機器的性能、業(yè)務場景來手動配置線程池的參數(shù)比如核心線程數(shù)、使用的任務隊列、飽和策略等等。 -
我們應該顯示地給我們的線程池命名,這樣有助于我們定位問題。
2、監(jiān)測線程池運行狀態(tài)
ThreadPoolExecutor 的相關 API 做一個簡陋的監(jiān)控。從下圖可以看出, ThreadPoolExecutor提供了獲取線程池當前的線程數(shù)和活躍線程數(shù)、已經(jīng)執(zhí)行完成的任務數(shù)、正在排隊中的任務數(shù)等等。
printThreadPoolStatus()會每隔一秒打印出線程池的線程數(shù)、活躍線程數(shù)、完成的任務數(shù)、以及隊列中的任務數(shù)。
/**
* 打印線程池的狀態(tài)
*
* @param threadPool 線程池對象
*/
public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) {
ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false));
scheduledExecutorService.scheduleAtFixedRate(() -> {
log.info("=========================");
log.info("ThreadPool Size: [{}]", threadPool.getPoolSize());
log.info("Active Threads: {}", threadPool.getActiveCount());
log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount());
log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());
log.info("=========================");
}, 0, 1, TimeUnit.SECONDS);
}
3、建議不同類別的業(yè)務用不同的線程池
4、別忘記給線程池命名
pool-1-thread-n 這樣的,沒有業(yè)務含義,不利于我們定位問題。
ThreadFactoryBuilder
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(threadNamePrefix + "-%d")
.setDaemon(true).build();
ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory)
ThreadFactor。
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 線程工廠,它設置線程名稱,有利于我們定位問題。
*/
public final class NamingThreadFactory implements ThreadFactory {
private final AtomicInteger threadNum = new AtomicInteger();
private final ThreadFactory delegate;
private final String name;
/**
* 創(chuàng)建一個帶名字的線程池生產(chǎn)工廠
*/
public NamingThreadFactory(ThreadFactory delegate, String name) {
this.delegate = delegate;
this.name = name; // TODO consider uniquifying this
}
@Override
public Thread newThread(Runnable r) {
Thread t = delegate.newThread(r);
t.setName(name + " [#" + threadNum.incrementAndGet() + "]");
return t;
}
}
5、正確配置線程池參數(shù)
常規(guī)操作
上下文切換: 多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用,為了讓這些線程都能得到有效執(zhí)行,CPU 采取的策略是為每個線程分配時間片并輪轉(zhuǎn)的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用,這個過程就屬于一次上下文切換。概括來說就是:當前任務在執(zhí)行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態(tài),以便下次再切換回這個任務時,可以再加載這個任務的狀態(tài)。任務從保存到再加載的過程就是一次上下文切換。 上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間,事實上,可能是操作系統(tǒng)中時間消耗最大的操作。 Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
-
如果我們設置的線程池數(shù)量太小的話,如果同一時間有大量任務/請求需要處理,可能會導致大量的請求/任務在任務隊列中排隊等待執(zhí)行,甚至會出現(xiàn)任務隊列滿了之后任務/請求無法處理的情況,或者大量任務堆積在任務隊列導致 OOM。這樣很明顯是有問題的,CPU 根本沒有得到充分利用。 -
如果我們設置線程數(shù)量太大,大量線程可能會同時在爭取 CPU 資源,這樣會導致大量的上下文切換,從而增加線程的執(zhí)行時間,影響了整體執(zhí)行效率。
-
CPU 密集型任務(N+1): 這種任務消耗的主要是 CPU 資源,可以將線程數(shù)設置為 N(CPU 核心數(shù))+1。比 CPU 核心數(shù)多出來的一個線程是為了防止線程偶發(fā)的缺頁中斷,或者其它原因?qū)е碌娜蝿諘和6鴰淼挠绊憽R坏┤蝿諘和#珻PU 就會處于空閑狀態(tài),而在這種情況下多出來的一個線程就可以充分利用 CPU 的空閑時間。 -
I/O 密集型任務(2N): 這種任務應用起來,系統(tǒng)會用大部分的時間來處理 I/O 交互,而線程在處理 I/O 的時間段內(nèi)不會占用 CPU 來處理,這時就可以將 CPU 交出給其它線程使用。因此在 I/O 密集型任務的應用中,我們可以多配置一些線程,具體的計算方法是 2N。
?? 拓展一下: 線程數(shù)更嚴謹?shù)挠嬎愕姆椒☉撌牵?code style="margin-right: 2px;margin-left: 2px;padding: 2px 4px;outline: 0px;color: rgb(40, 202, 113);font-size: 14px;line-height: 1.8em;letter-spacing: 0em;background: none 0% 0% / auto no-repeat scroll padding-box border-box rgba(27, 31, 35, 0.05);width: auto;height: auto;border-style: none;border-width: 3px;border-color: rgb(0, 0, 0) rgba(0, 0, 0, 0.4) rgba(0, 0, 0, 0.4);border-radius: 4px;font-family: Operator Mono, Consolas, Monaco, Menlo, monospace;word-break: break-all;">最佳線程數(shù) = N(CPU 核心數(shù))?(1+WT(線程等待時間)/ST(線程計算時間)),其中 WT(線程等待時間)=線程運行總時間 - ST(線程計算時間)。線程等待時間所占比例越高,需要越多線程。線程計算時間所占比例越高,需要越少線程。 我們可以通過 JDK 自帶的工具 VisualVM 來查看 WT/ST比例。CPU 密集型任務的 WT/ST接近或者等于 0,因此, 線程數(shù)可以設置為 N(CPU 核心數(shù))?(1+0)= N,和我們上面說的 N(CPU 核心數(shù))+1 差不多。IO 密集型任務下,幾乎全是線程等待時間,從理論上來說,你就可以將線程數(shù)設置為 2N(按道理來說,WT/ST 的結果應該比較大,這里選擇 2N 的原因應該是為了避免創(chuàng)建過多線程吧)。
美團的騷操作
-
corePoolSize: 核心線程數(shù)線程數(shù)定義了最小可以同時運行的線程數(shù)量。 -
maximumPoolSize: 當隊列中存放的任務達到隊列容量的時候,當前可以同時運行的線程數(shù)量變?yōu)樽畲缶€程數(shù)。 -
workQueue: 當新任務來的時候會先判斷當前運行的線程數(shù)量是否達到核心線程數(shù),如果達到的話,新任務就會被存放在隊列中。
ThreadPoolExecutor 提供的下面這些方法。
corePoolSize, 程序運行期間的時候,我們調(diào)用 setCorePoolSize()這個方法的話,線程池會首先判斷當前工作線程數(shù)是否大于corePoolSize,如果大于的話就會回收工作線程。
ResizableCapacityLinkedBlockIngQueue 的隊列(主要就是把LinkedBlockingQueue的 capacity 字段的 final 關鍵字修飾給去掉了,讓它變?yōu)榭勺兊模? -
Hippo-4[4] :一款強大的動態(tài)線程池框架,解決了傳統(tǒng)線程池使用存在的一些痛點比如線程池參數(shù)沒辦法動態(tài)修改、不支持運行時變量的傳遞、無法執(zhí)行優(yōu)雅關閉。除了支持動態(tài)修改線程池參數(shù)、線程池任務傳遞上下文,還支持通知報警、運行監(jiān)控等開箱即用的功能。 -
Dynamic TP[5] :輕量級動態(tài)線程池,內(nèi)置監(jiān)控告警功能,集成三方中間件線程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通過 SPI 自定義實現(xiàn))。
6、線程池使用的一些小坑
重復創(chuàng)建線程池的坑
@GetMapping("wrong")
public String wrong() throws InterruptedException {
// 自定義線程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,1L,TimeUnit.SECONDS,new ArrayBlockingQueue<>(100),new ThreadPoolExecutor.CallerRunsPolicy());
// 處理任務
executor.execute(() -> {
// ......
}
return "OK";
}
Spring 內(nèi)部線程池的坑
@Configuration
@EnableAsync
public class ThreadPoolExecutorConfig {
@Bean(name="threadPoolExecutor")
public Executor threadPoolExecutor(){
ThreadPoolTaskExecutor threadPoolExecutor = new ThreadPoolTaskExecutor();
int processNum = Runtime.getRuntime().availableProcessors(); // 返回可用處理器的Java虛擬機的數(shù)量
int corePoolSize = (int) (processNum / (1 - 0.2));
int maxPoolSize = (int) (processNum / (1 - 0.5));
threadPoolExecutor.setCorePoolSize(corePoolSize); // 核心池大小
threadPoolExecutor.setMaxPoolSize(maxPoolSize); // 最大線程數(shù)
threadPoolExecutor.setQueueCapacity(maxPoolSize * 1000); // 隊列程度
threadPoolExecutor.setThreadPriority(Thread.MAX_PRIORITY);
threadPoolExecutor.setDaemon(false);
threadPoolExecutor.setKeepAliveSeconds(300);// 線程空閑時間
threadPoolExecutor.setThreadNamePrefix("test-Executor-"); // 線程名字前綴
return threadPoolExecutor;
}
}
線程池和 ThreadLocal 共用的坑
ThreadLocal共用,可能會導致線程從ThreadLocal獲取到的是舊值/臟數(shù)據(jù)。這是因為線程池會復用線程對象,與線程對象綁定的類的靜態(tài)屬性 ThreadLocal 變量也會被重用,這就導致一個線程可能獲取到其他線程的ThreadLocal 值。
server.tomcat.max-threads=1
TransmittableThreadLocal(TTL)。TransmittableThreadLocal類繼承并加強了 JDK 內(nèi)置的InheritableThreadLocal類,在使用線程池等會池化復用線程的執(zhí)行組件情況下,提供ThreadLocal值的傳遞功能,解決異步執(zhí)行時上下文傳遞的問題。
InheritableThreadLocal 項目地址:https://github.com/alibaba/transmittable-thread-local。
推薦閱讀:
Spring Boot + URule 實現(xiàn)可視化規(guī)則引擎,太優(yōu)雅了!
程序員在線工具站:cxytools.com
推薦一個我自己寫的工具站:http://cxytools.com,專為程序員設計,包括時間日期、JSON處理、SQL格式化、隨機字符串生成、UUID生成、隨機數(shù)生成、文本Hash...等功能,提升開發(fā)效率。
?戳閱讀原文直達! 朕已閱 
