1、背景
在我們的專案中有這麼一個場景,需要消費kafka
中的訊息,並生成對應的工單資料。早些時候程式執行的好好的,但是有一天,我們升級了容器的配置
,結果導致部分訊息無法消費。而消費者的程式碼是使用CompletableFuture.runAsync(() -> {while (true){ ..... }})
來實現的。
即:
- 需要消費Kafka topic的個數: 7個,每個執行緒消費一個topic
- 消費方式:使用執行緒池非同步消費
- 消費池:預設的
ForkJoin
執行緒池???
,並且沒有做任何配置 - 是否會釋放執行緒池中的核心執行緒: 不會釋放
- 沒出問題時容器配置:
2核4G
- 出問題時容器配置:
4核8G
,影響的結果:只有3個topic
的資料可以消費。
2、容器2核4G可以正常消費
即:此時程式會啟動7個執行緒來進行消費。
3、容器4核8G只有部分可以消費
即:此時程式會啟動3個執行緒來進行消費。
4、問題原因分析
1、透過上面的背景
我們可以知道,是因為升級了容器的配置
,才導致我們消費kafka
中的訊息失敗了。
2、針對kafka
中的每個topic
,我們都會使用一個單獨的執行緒
來消費,並且不會釋放
這個執行緒。
3、而執行緒的啟動方式是透過CompletableFuture.runAsync()
方法來啟動的,那麼透過這種方式啟動的執行緒,是每個任務一個啟動一個執行緒,還是隻啟動固定的執行緒呢?
.
透過以上分析,那麼問題肯定是出現在執行緒池
身上,那麼我們預設使用的是什麼執行緒池呢?檢視CompletableFuture.runAsync()
的原始碼可知,有一定的機率是ForkJoinPool
。那麼我們一起看下原始碼。
5、原始碼分析
1、確認使用什麼執行緒池
public static CompletableFuture<Void> runAsync(Runnable runnable) {
return asyncRunStage(asyncPool, runnable);
}
private static final Executor asyncPool = useCommonPool ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
透過上述原始碼可知,我們可能使用的ForkJoin
執行緒池,也可能使用的是ThreadPerTaskExecutor
執行緒池。
ThreadPerTaskExecutor
這個是每個任務,一個執行緒。ForkJoinPool
那麼就需要確定啟動了多少個執行緒。
2、確認是否使用 ForkJoin 執行緒池
需要確定 useCommonPool
欄位是如何賦值的。
private static final boolean useCommonPool =
(ForkJoinPool.getCommonPoolParallelism() > 1);
透過上面程式碼可知,是否使用ForkJoin執行緒池,是由 ForkJoinPool.getCommonPoolParallelism()
的值確定的。(即並行度是否大於1,大於則使用ForkJoin執行緒池)
public static int getCommonPoolParallelism() {
return commonParallelism;
}
3、commonParallelism 的賦值
1、從上圖中可知parallelism
的設定有2種方式
- 透過Jvm的啟動引數
java.util.concurrent.ForkJoinPool.common.parallelism
進行設定,且這個值最大為MAX_CAP
即32727。 - 若沒有透過Jvm的引數配置,則有
2種情況
,若cpu的核數<=1,則返回1,否則返回cpu的核數-1
2、commonParallelism的取值
common = java.security.AccessController.doPrivileged
(new java.security.PrivilegedAction<ForkJoinPool>() {
public ForkJoinPool run() { return makeCommonPool(); }});
int par = common.config & SMASK; // report 1 even if threads disabled
commonParallelism = par > 0 ? par : 1;
SMASK
的值是 65535。
common.config
的值就是 (parallelism & SMASK) | 0
的值,即最大為65535,若parallelism的值為0,則返回0。
int par = common.config & SMASK
,即最大為 65535
commonParallelism = par > 0 ? par : 1
的值就為 parallelism
的值或1
6、結論
結論:
由上面的知識點,我們可以得出,當我們的容器是2核4G時,程式選擇的執行緒池是ThreadPerTaskExecutor
,當我們的容器是4核8G時,程式選擇的執行緒池是ForkJoinPool
。