1 배경
작업을 받은 후 주문 인터페이스를 최적화해야 하는데 비즈니스 로직을 확인한 후 병렬 또는 비동기로 쿼리할 수 있는 곳이 있다는 것을 알았으므로 CompletableFuture를 사용하여 비동기 최적화를 수행하여 인터페이스 응답 속도를 향상시켰습니다. 의사 코드는 다음과 같습니다
//查询用户信息
CompletableFuture<JSONObject> userInfoFuture = CompletableFuture
.supplyAsync(() -> proMemberService.queryUserById(ordOrder.getId()));
//查询积分商品信息
CompletableFuture<JSONObject> integralProInfoFuture = CompletableFuture
.supplyAsync(() -> proInfoService
.getProById(ordOrderIntegral.getProId()));
//查询会员积分信息
CompletableFuture<Integer> integerFuture = CompletableFuture
.supplyAsync(() -> proMemberService
.getTotalIntegralById(ordOrder.getOpenId()));
2번의 테스트
최적화 후 테스트 실행 속도가 2000ms에서 600ms로 떨어졌고 로컬 및 테스트 환경 테스트 후 프로덕션 로그에 스레드
번호가 출력되었지만 CompletableFuture의 기본 스레드 풀에서 가져오지 않았습니다.
로컬 및 테스트 환경 인쇄 로그.
프로덕션 환경이 각 스레드에 대해 새 스레드를 생성하는 로그에서 발견됨.동시성이 너무 높으면 스레드 리소스가 고갈되어 서버가 충돌할 가능성이 있습니다. .
3가지 이유
CompletableFuture의 소스 코드를 읽은 후 마침내 이유를 찾았습니다. 기본 ForkJoinPool 스레드 풀을 사용할지 여부는 시스템 구성과 관련이 있습니다 .
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
return asyncSupplyStage(ASYNC_POOL, supplier);
}
클릭하여 asynPool 입력
//是否使用默认线程池的判断依据
private static final Executor ASYNC_POOL = USE_COMMON_POOL ?
ForkJoinPool.commonPool() : new ThreadPerTaskExecutor();
//useCommonPool的来源
private static final boolean USE_COMMON_POOL =
(ForkJoinPool.getCommonPoolParallelism() > 1);
CompletableFuture가 기본 스레드 풀을 사용하는지 여부는 useCommonPool 값에 따라 판단되며 값은 true입니다.
public static int getCommonPoolParallelism() {
return COMMON_PARALLELISM;
}
public ForkJoinPool() {
this(Math.min(MAX_CAP, Runtime.getRuntime().availableProcessors()),
defaultForkJoinWorkerThreadFactory, null, false,
0, MAX_CAP, 1, null, DEFAULT_KEEPALIVE, TimeUnit.MILLISECONDS);
}
public ForkJoinPool(int parallelism,
ForkJoinWorkerThreadFactory factory,
UncaughtExceptionHandler handler,
boolean asyncMode,
int corePoolSize,
int maximumPoolSize,
int minimumRunnable,
Predicate<? super ForkJoinPool> saturate,
long keepAliveTime,
TimeUnit unit) {
checkPermission();
int p = parallelism;
if (p <= 0 || p > MAX_CAP || p > maximumPoolSize || keepAliveTime <= 0L)
throw new IllegalArgumentException();
if (factory == null || unit == null)
throw new NullPointerException();
this.factory = factory;
this.ueh = handler;
this.saturate = saturate;
this.keepAlive = Math.max(unit.toMillis(keepAliveTime), TIMEOUT_SLOP);
int size = 1 << (33 - Integer.numberOfLeadingZeros(p - 1));
int corep = Math.min(Math.max(corePoolSize, p), MAX_CAP);
int maxSpares = Math.min(maximumPoolSize, MAX_CAP) - p;
int minAvail = Math.min(Math.max(minimumRunnable, 0), MAX_CAP);
this.bounds = ((minAvail - p) & SMASK) | (maxSpares << SWIDTH);
this.mode = p | (asyncMode ? FIFO : 0);
this.ctl = ((((long)(-corep) << TC_SHIFT) & TC_MASK) |
(((long)(-p) << RC_SHIFT) & RC_MASK));
this.registrationLock = new ReentrantLock();
this.queues = new WorkQueue[size];
String pid = Integer.toString(getAndAddPoolIds(1) + 1);
this.workerNamePrefix = "ForkJoinPool-" + pid + "-worker-";
}
4. 요약
- CompletableFuture를 사용하려면 스레드 풀을 사용자 지정해야 합니다.
- CompletableFuture가 기본 스레드 풀을 사용하는지 여부는 머신 코어 수와 관련이 있습니다. 기본 스레드 풀은 코어 수에서 1을 뺀 값이 1보다 큰 경우에만 사용되며, 그렇지 않으면 각 작업에 대해 스레드가 생성됩니다.
- 서버 코어가 2보다 크고 기본 스레드 풀을 사용하더라도 스레드 풀에 너무 적은 수의 스레드가 있을 수 있으므로 많은 수의 스레드가 대기하고 처리량이 줄어들며 서버를 끌어내릴 수도 있습니다.
- ForkJoinPool은 CPU를 많이 사용하는 작업(계산)에 사용됩니다.
프로세서 사용률에 대한 스레드 풀 크기의 비율은 다음 공식을 사용하여 추정할 수 있습니다.
N 스레드 = N CPU * U CPU * (1 + W/C)
여기에서:
-
NCPU는 Runtime.getRuntime().availableProcessors()를 통해 얻을 수 있는 프로세서의 코어 수입니다. -
U CPU는 원하는 CPU 사용률입니다(값은 0과 1 사이여야 함).
-
W/C는 계산 시간에 대한 대기 시간의 비율입니다.
스레드 풀의 크기를 설정하는 일반적인 규칙은 다음과 같습니다.
-
서비스가 CPU를 많이 사용하는 경우 컴퓨터의 코어 수로 설정합니다.
-
서비스가 io 집약적인 경우 컴퓨터의 코어 수*2로 설정합니다.