Concurrent Programming - CompletableFuture Analysis | Jingdong Logistics Technical Team

1. Introduction to Completable Future

The CompletableFuture object is a newly introduced class in JDK1.8. This class implements two interfaces, one is the Future interface and the other is the CompletionStage interface.

The CompletionStage interface is an interface provided by JDK1.8, which is used for stage processing in asynchronous execution. CompletionStage defines a set of interfaces used to either continue to execute the next stage after the execution of a stage is completed, or convert the result to generate a new one. Results, etc. Generally speaking, to execute the next stage, the previous stage needs to be completed normally. This class also provides an interface for processing abnormal results.

2、CompletableFuture的API

2.1 Submit a task

There are several ways to submit tasks in CompletableFuture:

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)

These four methods are used to submit tasks. The difference is that the tasks submitted by supplyAsync have return values, and the tasks submitted by runAsync have no return values. Both interfaces have an overloaded method, and the second input parameter is the specified thread pool. If not specified, the ForkJoinPool.commonPool() thread pool will be used by default. In the process of use, try to designate different thread pools according to different businesses, so as to facilitate the monitoring of different thread pools, and at the same time avoid the mutual influence of the shared thread pools of businesses.

2.2 Result Transformation

2.2.1 thenApply

public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

The input parameter of thenApply is Function, which means that the last CompletableFuture execution result is used as an input parameter, converted or calculated again, and returns a new value.

2.2.2 handle

public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn)
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor)

The input parameter of the handle group is BiFunction. This functional interface has two input parameters and one return value, which means to process the processing result of the previous CompletableFuture. At the same time, if there is an exception, you need to manually handle the exception.

2.2.3 thenRun

public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action, Executor executor)

The input parameter of the thenRun group of functions is the Runnable functional interface. This interface does not require input and output parameters. This group of functions executes another interface after the execution of the previous CompletableFuture task, and does not require the result of the previous task. There is no need to return a value, it only needs to be executed after the previous task is executed.

2.2.4 thenAccept

public CompletableFuture<Void> thenAccept(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action)
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action, Executor executor)

The input parameter of the thenAccept group of functions is Consumer. This functional interface has one input parameter and no return value, so this group of interfaces means to process the processing result of the previous CompletableFuture, but not return the result.

2.2.5 thenAcceptBoth

public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action)
public <U> CompletableFuture<Void> thenAcceptBothAsync(CompletionStage<? extends U> other, BiConsumer<? super T, ? super U> action, Executor executor)

The input parameters of thenAcceptBoth function include CompletionStage and BiConsumer. CompletionStage is a new interface of JDK1.8. There is only one implementation class in JDK: CompletableFuture, so the first input parameter is CompletableFuture. This group of functions is used to accept two CompletableFuture return values, and combine them together. The BiConsumer functional interface has two input parameters and has no return value. The first input parameter of BiConsumer is the execution result of the CompletableFuture of the caller, and the second input parameter is the execution result of the CompletableFuture input parameter of the thenAcceptBoth interface. So this group of functions means to merge the execution results of two CompletableFutures together.

2.2.6 thenCombine

public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn, Executor executor)

The thenCombine group of functions is similar to thenAcceptBoth. The input parameters all contain a CompletionStage, which is a CompletableFuture object, which means to combine the execution results of two CompletableFutures. The difference is that the second input parameter of thenCombine is BiFunction. This functional interface has two input parameters and a return value. So different from thenAcceptBoth, thenCombine will return a new value as an output parameter after combining the two task results.

2.2.7 thenCompose

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn)
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor)

The thenCompose group of functions means that the execution result of the caller is used as the input parameter of the Function function, and a new CompletableFuture object is returned at the same time.

2.3 Callback method

public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)

The whenComplete method means that the method is executed after the execution of the previous CompletableFuture object task is completed. The BiConsumer functional interface has two input parameters with no return value. The first of these two input parameters is the execution result of the CompletableFuture task, and the second is the exception information. Indicates the result of processing the previous task. If there is an exception, you need to manually handle the exception. The difference from the handle method is that the BiFunction of the handle method has a return value, while BiConsumer has no return value.

The above methods all have a method with Async. The method with Async means that it is executed asynchronously, and the task will be executed in the thread pool. At the same time, this method will have an overloaded method, and the last parameter is Executor. Indicates that asynchronous execution can specify thread pool execution. For ease of control, it's best to manually specify our thread pool when using CompletableFuture.

2.4 Exception handling

public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

exceptionally is used to handle exceptions. When a task throws an exception, it can be processed through exceptionally or handle, but the two are somewhat different. Hand is used to process the result of the previous task. If there is If there is an exception, handle the exception. And exceptionally can be placed at the end of CompletableFuture processing, as a bottom-up logic to handle unknown exceptions.

2.5 Get results

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)

allOf requires all CompletableFuture tasks in the input parameters to be executed before proceeding to the next step;

anyOf means that any CompletableFuture task in the input parameter can execute the next step after execution.

public T get() throws InterruptedException, ExecutionException
public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
public T getNow(T valueIfAbsent)
public T join()

One of the get methods is without a timeout, and the other is with a timeout.

The getNow method returns the result immediately. If there is no result yet, it returns the default value, which is the input parameter of this method.

The join method is to wait for the task to complete without a timeout.

3. Principle of Completable Future

The join method also means to obtain the result, but what is the difference between the join and get methods.

public T join() {
    Object r;
    return reportJoin((r = result) == null ? waitingGet(false) : r);
}

public T get() throws InterruptedException, ExecutionException {
    Object r;
    return reportGet((r = result) == null ? waitingGet(true) : r);
}

public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        Object r;
        long nanos = unit.toNanos(timeout);
        return reportGet((r = result) == null ? timedGet(nanos) : r);
}

public T getNow(T valueIfAbsent) {
        Object r;
        return ((r = result) == null) ? valueIfAbsent : reportJoin(r);
}

The above is the code of the two methods in the CompletableFuture class. You can see that the two methods are almost the same. The difference is that the reportJoin/reportGet and waitingGet methods are the same, but the parameters are different. Let's look at the reportGet and reportJoin methods.

private static <T> T reportGet(Object r)
        throws InterruptedException, ExecutionException {
        if (r == null) // by convention below, null means interrupted
            throw new InterruptedException();
        if (r instanceof AltResult) {
            Throwable x, cause;
            if ((x = ((AltResult)r).ex) == null)
                return null;
            if (x instanceof CancellationException)
                throw (CancellationException)x;
            if ((x instanceof CompletionException) &&
                (cause = x.getCause()) != null)
                x = cause;
            throw new ExecutionException(x);
        }
        @SuppressWarnings("unchecked") T t = (T) r;
        return t;
    }
private static <T> T reportJoin(Object r) {
        if (r instanceof AltResult) {
            Throwable x;
            if ((x = ((AltResult)r).ex) == null)
                return null;
            if (x instanceof CancellationException)
                throw (CancellationException)x;
            if (x instanceof CompletionException)
                throw (CompletionException)x;
            throw new CompletionException(x);
        }
        @SuppressWarnings("unchecked") T t = (T) r;
        return t;
    }

It can be seen that these two methods are very similar. The reportGet method judges whether the r object is empty, and throws an interrupt exception, but the reportJoin method does not judge, and reportJoin throws all runtime exceptions, so the join method does not need to be manually Catch the exception.

We are looking at the waitingGet method

private Object waitingGet(boolean interruptible) {
        Signaller q = null;
        boolean queued = false;
        int spins = -1;
        Object r;
        while ((r = result) == null) {
            if (spins < 0)
                spins = SPINS;
            else if (spins > 0) {
                if (ThreadLocalRandom.nextSecondarySeed() >= 0)
                    --spins;
            }
            else if (q == null)
                q = new Signaller(interruptible, 0L, 0L);
            else if (!queued)
                queued = tryPushStack(q);
            else if (interruptible && q.interruptControl < 0) {
                q.thread = null;
                cleanStack();
                return null;
            }
            else if (q.thread != null && result == null) {
                try {
                    ForkJoinPool.managedBlock(q);
                } catch (InterruptedException ie) {
                    q.interruptControl = -1;
                }
            }
        }
        if (q != null) {
            q.thread = null;
            if (q.interruptControl < 0) {
                if (interruptible)
                    r = null; // report interruption
                else
                    Thread.currentThread().interrupt();
            }
        }
        postComplete();
        return r;
    }

The waitingGet method loops through while to determine whether the task has been completed and produces a result. If the result is empty, it will always loop here. It should be noted here that spins=-1 is initialized here. When entering for the first time During the while loop, the spins is -1, and the spins will be assigned a constant value, which is SPINS.

private static final int SPINS = (Runtime.getRuntime().availableProcessors() > 1 ?
                                      1 << 8 : 0);

Here it is judged whether the number of available CPUs is greater than 1. If it is greater than 1, the constant is 1<<8, which is 256, otherwise the constant is 0.

When entering the while loop for the second time, the spins are 256 greater than 0, and the operation of subtracting one is done here. The next time you enter the while loop, if there is no result, it is still greater than 0 and continue to perform the operation of subtracting one, which is used here. Spin for a short time and wait for the result. Only when spins is equal to 0, it will enter the normal process of judgment.

We are looking at the source code of the timedGet method

private Object timedGet(long nanos) throws TimeoutException {
        if (Thread.interrupted())
            return null;
        if (nanos <= 0L)
            throw new TimeoutException();
        long d = System.nanoTime() + nanos;
        Signaller q = new Signaller(true, nanos, d == 0L ? 1L : d); // avoid 0
        boolean queued = false;
        Object r;
        // We intentionally don't spin here (as waitingGet does) because
        // the call to nanoTime() above acts much like a spin.
        while ((r = result) == null) {
            if (!queued)
                queued = tryPushStack(q);
            else if (q.interruptControl < 0 || q.nanos <= 0L) {
                q.thread = null;
                cleanStack();
                if (q.interruptControl < 0)
                    return null;
                throw new TimeoutException();
            }
            else if (q.thread != null && result == null) {
                try {
                    ForkJoinPool.managedBlock(q);
                } catch (InterruptedException ie) {
                    q.interruptControl = -1;
                }
            }
        }
        if (q.interruptControl < 0)
            r = null;
        q.thread = null;
        postComplete();
        return r;
    }

The timedGet method still uses the while loop to judge whether it has been completed. The timedGet method takes a nanosecond value as the input parameter, and calculates a deadline deadline through this value. When the while loop has not yet obtained the task result and has reached the deadline , a TimeoutException is thrown.

4. CompletableFuture implements multi-threaded tasks

Here we use CompletableFuture to implement an example of multi-threaded processing of asynchronous tasks.

Here we create 10 tasks and submit them to our specified thread pool for execution, and wait for all 10 tasks to be executed.

The execution flow of each task is to perform addition first, and then perform multiplication for the second time. If an exception occurs, the default value will be returned. After the execution of 10 tasks is completed, the results of each task will be printed in turn.

public void demo() throws InterruptedException, ExecutionException, TimeoutException {
        // 1、自定义线程池
        ExecutorService executorService = new ThreadPoolExecutor(5, 10,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(100));

        // 2、集合保存future对象
        List<CompletableFuture<Integer>> futures = new ArrayList<>(10);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            CompletableFuture<Integer> future = CompletableFuture
                    // 提交任务到指定线程池
                    .supplyAsync(() -> this.addValue(finalI), executorService)
                    // 第一个任务执行结果在此处进行处理
                    .thenApplyAsync(k -> this.plusValue(finalI, k), executorService)
                    // 任务执行异常时处理异常并返回默认值
                    .exceptionally(e -> this.defaultValue(finalI, e));
            // future对象添加到集合中
            futures.add(future);
        }

        // 3、等待所有任务执行完成,此处最好加超时时间
        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).get(5, TimeUnit.MINUTES);
        for (CompletableFuture<Integer> future : futures) {
            Integer num = future.get();
            System.out.println("任务执行结果为:" + num);
        }
        System.out.println("任务全部执行完成!");
    }

    private Integer addValue(Integer index) {
        System.out.println("第" + index + "个任务第一次执行");
        if (index == 3) {
            int value = index / 0;
        }
        return index + 3;
    }

    private Integer plusValue(Integer index, Integer num) {
        System.out.println("第" + index + "个任务第二次执行,上次执行结果:" + num);
        return num * 10;
    }

    private Integer defaultValue(Integer index, Throwable e) {
        System.out.println("第" + index + "个任务执行异常!" + e.getMessage());
        e.printStackTrace();
        return 10;
    }

Author: JD Logistics Ding Dong

Source: JD Cloud developer community Ziqishuo Tech

Musk announced that Twitter will change its name to X and replace the Logo . React core developer Dan Abramov announced his resignation from Meta Clarification about MyBatis-Flex plagiarizing MyBatis-Plus OpenAI officially launched the Android version of ChatGPT ChatGPT for Android will be launched next week, now Started pre-registration Arc browser officially released 1.0, claiming to be a replacement for Chrome Musk "purchased for zero yuan", robbed @x Twitter account VS Code optimized name obfuscation compression, reduced built-in JS by 20%! Bun 0.7, a new high-speed JavaScript runtime , was officially released
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/10091428
Recommended