ForkJoinPool在生產環境中使用遇到的一個問題

huan1993發表於2024-03-24

1、背景

在我們的專案中有這麼一個場景,需要消費kafka中的訊息,並生成對應的工單資料。早些時候程式執行的好好的,但是有一天,我們升級了容器的配置,結果導致部分訊息無法消費。而消費者的程式碼是使用CompletableFuture.runAsync(() -> {while (true){ ..... }}) 來實現的。
即:

  1. 需要消費Kafka topic的個數: 7個,每個執行緒消費一個topic
  2. 消費方式:使用執行緒池非同步消費
  3. 消費池:預設的 ForkJoin 執行緒池???,並且沒有做任何配置
  4. 是否會釋放執行緒池中的核心執行緒: 不會釋放
  5. 沒出問題時容器配置: 2核4G
  6. 出問題時容器配置:4核8G,影響的結果:只有3個topic的資料可以消費。

2、容器2核4G可以正常消費

容器2核4G可以正常消費

即:此時程式會啟動7個執行緒來進行消費。

3、容器4核8G只有部分可以消費

容器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執行緒池。

  1. ThreadPerTaskExecutor 這個是每個任務,一個執行緒。
  2. 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 的賦值

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

相關文章