1-概述
1.1 背景
企知道後臺服務存在大量的查詢可以併發,大量用到了java8的CompletableFuture特性,但是在性能測試中,遇到了併發的瓶頸。
經過分析,發現是由於CompletableFuture默認線程池以及公共線程池核心線程數過少,在重度IO操作、高併發場景下引起線程競爭、排隊,從而產生大量慢請求。
1.2 事實
本文通過代碼分析及測試驗證,得出以下兩個結論:
- 在多核(>2核)環境下,CompletableFuture的默認線程數量=可用CPU數-1;
- 默認線程數可以通過虛擬機參數 java.util.concurrent.ForkJoinPool.common.parallelism 來指定。
1.3 建議
在IO密集型場景中使用CompletableFuture來實現高併發時,採用以下方法之一:
方法一:使用CompletableFuture默認線程池,並指定 java.util.concurrent.ForkJoinPool.common.parallelism 爲一個較大的值(>1000ms/線程平均使用時間/併發任務數*期望QPS);
方法二:傳入自定義線程池,且自定義線程池的核心線程數使用較大的值(同上),或者使用合理的最大線程&隊列組合(既要避免難觸發擴容,也要避免頻繁擴縮容)。
2-分析過程
2.1 查看CompetableFuture.runAsync的代碼,發現在不傳入自定義線程池的情況下,其使用了名爲ASYNC_POOL的默認線程池:
public static CompletableFuture<Void> runAsync(Runnable runnable) { return asyncRunStage(ASYNC_POOL, runnable); }
2.2 ASYNC_POOL則在默認使用ForkJoinPool.commonPool()線程池,如果線程池只有一個線程,則退化爲不使用線程池可每次創建新線程:
private static final boolean USE_COMMON_POOL = (ForkJoinPool.getCommonPoolParallelism() > 1);
/** * Default executor -- ForkJoinPool.commonPool() unless it cannot * support parallelism. */ private static final Executor ASYNC_POOL = USE_COMMON_POOL ? ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
/** Fallback if ForkJoinPool.commonPool() cannot support parallelism */ static final class ThreadPerTaskExecutor implements Executor { public void execute(Runnable r) { Objects.requireNonNull(r); new Thread(r).start(); } }
2.3 ForkJoinPool.commonPool()線程池線程數量=可用CPU數-1,也可以通過虛擬機參數來指定
//commonPool方法返回common靜態成員
public static ForkJoinPool commonPool() { // assert common != null : "static init error"; return common; } //common本身是一個ForkJoinPool的靜態單例成員常量
static final ForkJoinPool common;
//common在靜態構造函數中初始化 static { ... @SuppressWarnings("removal") ForkJoinPool tmp = AccessController.doPrivileged(new PrivilegedAction<>() { public ForkJoinPool run() { return new ForkJoinPool((byte)0); }}); common = tmp; COMMON_PARALLELISM = Math.max(common.mode & SMASK, 1); }
//ForkJoinPool的併發度,默認值=可用邏輯CPU數-1,也可以通過系統屬性來指定,最大不超過MAX_CAP(0x7ffff=524287)
private ForkJoinPool(byte forCommonPoolOnly) { int parallelism = Math.max(1, Runtime.getRuntime().availableProcessors() - 1); ... try { // ignore exceptions in accessing/parsing properties ... String pp = System.getProperty ("java.util.concurrent.ForkJoinPool.common.parallelism"); if (pp != null) parallelism = Integer.parseInt(pp); } catch (Exception ignore) { } ... int p = Math.min(Math.max(parallelism, 0), MAX_CAP), size; this.mode = p; ... }
3-代碼驗證
3.1 驗證默認線程數
在我8核的MacBookAir中,運行以下代碼:
public class TestMain { public static void main(String[] args) throws ExecutionException, InterruptedException { final int times = 100; CompletableFuture<?>[] futures = new CompletableFuture[times]; //發起100個異步任務 for (int i = 0; i < times; i++) { // Lambda中不能使用外層非只讀變量 final int j = i; futures[i] = CompletableFuture.runAsync(() -> { //打印當前線程名 System.out.printf("[%s]%03d:%s\n", LocalDateTime.now(), j, Thread.currentThread().getName()); try { //阻塞線程 Thread.sleep(1000); } catch (InterruptedException e) { throw new RuntimeException(e); } }); } //等待線程完成,沒有這句則主線程退出會直接退出 CompletableFuture.allOf(futures).join(); } }
運行結果(順序不一定如此):
[2024-04-09T10:55:17.187205]004:ForkJoinPool.commonPool-worker-5
[2024-04-09T10:55:17.189296]003:ForkJoinPool.commonPool-worker-4
[2024-04-09T10:55:17.188725]000:ForkJoinPool.commonPool-worker-1
[2024-04-09T10:55:17.188861]005:ForkJoinPool.commonPool-worker-6
[2024-04-09T10:55:17.188015]002:ForkJoinPool.commonPool-worker-3
[2024-04-09T10:55:17.187862]006:ForkJoinPool.commonPool-worker-7
[2024-04-09T10:55:17.186498]001:ForkJoinPool.commonPool-worker-2
[2024-04-09T10:55:18.249665]007:ForkJoinPool.commonPool-worker-6
[2024-04-09T10:55:18.254584]008:ForkJoinPool.commonPool-worker-2
[2024-04-09T10:55:18.254585]011:ForkJoinPool.commonPool-worker-7
[2024-04-09T10:55:18.254582]010:ForkJoinPool.commonPool-worker-4
[2024-04-09T10:55:18.254584]009:ForkJoinPool.commonPool-worker-1
[2024-04-09T10:55:19.258823]012:ForkJoinPool.commonPool-worker-6
[2024-04-09T10:55:19.260827]013:ForkJoinPool.commonPool-worker-4
[2024-04-09T10:55:19.261448]014:ForkJoinPool.commonPool-worker-2
[2024-04-09T10:55:20.261756]015:ForkJoinPool.commonPool-worker-6
[2024-04-09T10:55:20.265064]017:ForkJoinPool.commonPool-worker-2
[2024-04-09T10:55:20.263193]016:ForkJoinPool.commonPool-worker-4
...
很容易看出來,一直只有7(即8-1)個線程在運行任務。
3.2 驗證使用虛擬機參數指定線程數量
在命令行中,運行命令修改如下
java -Djava.util.concurrent.ForkJoinPool.common.parallelism=3 -classpath xxxx/TestMain.class TestMain
如果在IDEA中,可以在 運行配置-構建並運行-修改選項-添加虛擬機參數 打開虛擬機參數輸入框,並填入: -Djava.util.concurrent.ForkJoinPool.common.parallelism=3
再次運行驗證代碼,輸出如下:
[2024-04-09T11:00:26.626672]001:ForkJoinPool.commonPool-worker-2
[2024-04-09T11:00:26.626556]002:ForkJoinPool.commonPool-worker-3
[2024-04-09T11:00:26.626734]000:ForkJoinPool.commonPool-worker-1
[2024-04-09T11:00:27.671474]003:ForkJoinPool.commonPool-worker-2
[2024-04-09T11:00:27.673111]004:ForkJoinPool.commonPool-worker-1
[2024-04-09T11:00:27.673024]005:ForkJoinPool.commonPool-worker-3
[2024-04-09T11:00:28.675585]006:ForkJoinPool.commonPool-worker-2
[2024-04-09T11:00:28.676933]007:ForkJoinPool.commonPool-worker-1
[2024-04-09T11:00:28.676961]008:ForkJoinPool.commonPool-worker-3
...
我們發現虛擬機參數確實生效了,ForkJoinPool只有3個線程在運行任務。