Issues that must be paid attention to when using CompletableFuture in production

Issues that must be paid attention to when using CompletableFuture in production

1 background

After receiving a task, I need to optimize the order interface. After checking the business logic, I found that there are some places that can be queried in parallel or asynchronously, so I use CompletableFuture to do asynchronous optimization to improve the interface response speed. The pseudo code is as follows

//查询用户信息
            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 tests

After optimization, the test execution speed dropped from 2000ms to 600ms. After the local and test environment tests, the production log
printed out the thread number, but it was not taken from the default thread pool of CompletableFuture.
Production log
insert image description here

Local and test environment print logs.
insert image description here
It is found in the log that the production environment creates a new thread for each thread. If the concurrency is too high, there is a possibility that thread resources will be exhausted, which will cause the server to crash.

3 reasons

After reading the source code of CompletableFuture, I finally found the reason: whether to use the default ForkJoinPool thread pool is related to the machine configuration .

public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier) {
    
    
        return asyncSupplyStage(ASYNC_POOL, supplier);
    }

Click to enter 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);

Whether CompletableFuture uses the default thread pool is judged according to the useCommonPool value, and the value is 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. Summary

  • To use CompletableFuture, you must customize the thread pool
  • Whether CompletableFuture uses the default thread pool is related to the number of machine cores. The default thread pool will only be used if the number of cores minus 1 is greater than 1, otherwise a thread is created for each task.
  • Even if the server core is greater than 2 and the default thread pool is used, there may be too few threads in the thread pool, causing a large number of threads to wait, reducing throughput, and even dragging down the server
  • ForkJoinPool is used for CPU-intensive tasks (calculations).

The ratio of thread pool size to processor utilization can be estimated using the following formula:
N threads = N CPU * U CPU * (1 + W/C)
where:

  • NCPU is the number of cores of the processor, which can be
    obtained through Runtime.getRuntime().availableProcessors()

  • U CPU is the desired CPU utilization (the value should be between 0 and 1)

  • W/C is the ratio of waiting time to computing time

The general rules for setting the size of the thread pool are:

  • If the service is cpu-intensive, set it to the number of cores of the computer

  • If the service is io-intensive, set it to the number of cores of the computer*2

Guess you like

Origin blog.csdn.net/weixin_42202992/article/details/131990901