Detailed explanation of multi-threaded asynchronous orchestration examples

1. Review of multi-threading

Table of contents

1. Review of multi-threading

1. 4 ways to initialize threads

2. Seven parameters of thread pool

3. Four common types of thread pools

4. Why use thread pool in development?

2. CompletableFuture asynchronous orchestration

1. Create an asynchronous object

2. Callback method when calculation is completed 

3. handle method

4. Thread serialization method

5. Two task combinations - both must be completed

6. Two task combination - one completed

7. Multi-tasking combination


 

1. 4 ways to initialize threads

1), inherit Thread

Thread thread = new Thread01();
thread.start();

2), implement the Runnable interface

Runnable01 runnable01 = new Runnable01();
new Thread(runnable01).start();

3) Implement the Callable interface + FutureTask (you can get the return result and handle exceptions)

FutureTask futureTask = new FutureTask<>(new Callable01());
new Thread(futureTask).start();
// 阻塞等待线程执行完成,获取返回结果
Integer i = (Integer) futureTask.get();

4), thread pool

public static ExecutorService service = Executors.newFixedThreadPool(10);
service.execute(new Runnable01());

        Initialize the thread pool in the following two ways:

Executors.newFiexedThreadPool(3);
//或者
new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit unit, workQueue, threadFactory, 
Difference:
 Thread and Runnable cannot obtain the return value, and Callable can obtain the return value. 
Thread, Runnable, and Callable cannot control resources, but the thread pool can control resources. 
Through the thread pool, the performance is stable, execution results can also be obtained, and exceptions can be caught. However, in complex business situations, one asynchronous call may depend on the execution result of another asynchronous call.

2. Seven parameters of thread pool

/**
     * Creates a new {@code ThreadPoolExecutor} with the given initial
     * parameters.
     *
     * @param corePoolSize the number of threads to keep in the pool, even
     *        if they are idle, unless {@code allowCoreThreadTimeOut} is set
     * @param maximumPoolSize the maximum number of threads to allow in the
     *        pool
     * @param keepAliveTime when the number of threads is greater than
     *        the core, this is the maximum time that excess idle threads
     *        will wait for new tasks before terminating.
     * @param unit the time unit for the {@code keepAliveTime} argument
     * @param workQueue the queue to use for holding tasks before they are
     *        executed.  This queue will hold only the {@code Runnable}
     *        tasks submitted by the {@code execute} method.
     * @param threadFactory the factory to use when the executor
     *        creates a new thread
     * @param handler the handler to use when execution is blocked
     *        because the thread bounds and queue capacities are reached
     * @throws IllegalArgumentException if one of the following holds:<br>
     *         {@code corePoolSize < 0}<br>
     *         {@code keepAliveTime < 0}<br>
     *         {@code maximumPoolSize <= 0}<br>
     *         {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException if {@code workQueue}
     *         or {@code threadFactory} or {@code handler} is null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

Seven core parameters: 

corePoolSize: The number of core threads, the number of threads that are ready after the thread pool is created, waiting to receive asynchronous tasks.

maximumPoolSize: maximum number of threads, control resources

keepAliveTime: survival time. If the current number of threads is greater than the number of core threads, if the idle time of the thread is greater than the specified keepAliveTime, the thread will be released (maximum number of threads - number of core threads)

unit: time unit

BlockingQueue<Runnable> workQueue: blocking queue. If there are too many tasks, the current number of tasks will be placed in the queue (tasks submitted using Runable execute). When there are idle threads, the tasks will be taken out of the queue and executed.

ThreadFactory: Factory for creating threads

RejectedExecutionHandler handler: If the queue is full, refuse to execute the task according to the specified rejection policy

Unbounded queue and bounded queue in blocking queue:

Bounded queue : It is a queue with a fixed size. For example, a LinkedBlockingQueue with a fixed size is set, or a SynchronousQueue with a size of 0 is only used for transfer between producers and consumers.

Unbounded queue : refers to a queue that does not have a fixed size. The characteristic of these queues is that they can be enqueued directly until overflowing. Of course, reality rarely has such a large capacity (exceeding Integer.MAX_VALUE), so from the user experience, it is equivalent to "unbounded". For example, there is no fixed size LinkedBlockingQueue set.

*Running process:

1. Create a thread pool, prepare a core number of core threads, and prepare to accept tasks.

2. When new tasks come in, they are executed using idle threads prepared by the core.

        (1) If the core is full, the incoming tasks will be put into the blocking queue. The idle core will block the queue to obtain task execution.

        (2) When the blocking queue is full, a new thread will be opened directly for execution. The maximum number can only be opened up to the number specified by max.

        (3) and max are all executed. Max-core number of idle threads will be automatically destroyed after the time specified by keepAliveTime. Finally maintained to the core size.

        (4) If the number of threads reaches the max number and new tasks come in, the rejection policy specified by reject will be used for processing.

3. All threads are created by the specified factory

3. Four common types of thread pools

newCachedThreadPool: Create a cacheable thread pool. If the length of the thread pool exceeds processing needs, idle threads can be flexibly recycled. If there is no way to recycle, a new thread will be created.

newFixedThreadPool: Create a fixed-length thread pool that can control the maximum number of concurrent threads. Exceeding threads will wait in the queue.

newScheduledThreadPool: Create a fixed-length thread pool to support scheduled and periodic task execution.

newSingleThreadExecutor: Create a single-threaded thread pool, which will only use a unique worker thread to execute tasks, ensuring that all tasks are executed in the specified order (FIFO, LIFO, priority)

4. Why use thread pool in development?

  • Reduce resource consumption: reduce the loss caused by thread creation and destruction by reusing already created threads
  • Improve response speed: Because when the number of threads in the thread pool does not exceed the maximum limit of the thread pool, some threads are in a state of waiting for assigned tasks. When the tasks come, they can be executed without creating new threads.
  • Improve thread manageability: The thread pool will optimize the threads in the pool based on the current system characteristics to reduce the system overhead caused by creating and destroying threads. Unlimited creation and destruction of threads not only consumes system resources, but also reduces the stability of the system. Use the thread pool for unified allocation.

2. CompletableFuture asynchronous orchestration

        Business scenario: The logic of querying the product details page is relatively complex, and some data needs to be called remotely, which will inevitably take more time.

5ca33d7bce6d4506a6050cc827358db7.png

       If each query on the product details page requires the time marked below to be completed, then it will take 5.5 seconds for the user to see the content of the product details page. Obviously this is unacceptable. If multiple threads complete these 6 steps at the same time, it may only take 1.5s to complete the response.


        Future is a class added in Java 5 to describe the result of an asynchronous calculation. You can use the `isDone` method to check whether the calculation is completed, or use `get` to block the calling thread until the calculation is completed and return the result. You can also use the `cancel` method to stop the execution of the task.

         Although `Future` and related usage methods provide the ability to execute tasks asynchronously, it is very inconvenient to obtain the results. The results of the tasks can only be obtained through blocking or polling. The blocking method is obviously contrary to the original intention of our asynchronous programming. The polling method consumes unnecessary CPU resources and cannot obtain the calculation results in time. Why can't we use the observer design pattern to notify the listener in time when the calculation results are completed? Woolen cloth?

         Many languages, such as Node.js, use callbacks to implement asynchronous programming. Some Java frameworks, such as Netty, extend Java's `Future` interface and provide multiple extension methods such as `addListener`; Google guava also provides a general extended Future; Scala also provides an easy-to-use and powerful Future/Promise asynchronous programming model.

        As an orthodox Java class library, should we do something to enhance the functions of our own library?

        In Java 8, a new class containing about 50 methods has been added: CompletableFuture, which provides very powerful Future extension functions, can help us simplify the complexity of asynchronous programming, and provides functional programming capabilities through callbacks. The calculation results are processed in a way, and methods for converting and combining CompletableFuture are provided. The CompletableFuture class implements the Future interface, so you can still get the results by blocking or polling with the `get` method as before, but this method is not recommended.

        CompletableFuture and FutureTask both belong to the implementation class of the Future interface, and both can obtain the execution results of the thread.

5329b3b6a74849139d7d57b8824a7687.png

1. Create an asynchronous object

CompletableFuture provides four static methods to create an asynchronous operation.

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)

1. runXxxx does not return results, but supplyXxx can obtain return results.

2. You can pass in a custom thread pool, otherwise the default thread pool will be used;

2. Callback method when calculation is completed 

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)
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn)

whenComplete can handle normal and abnormal calculation results (success callback), and exceptionally handles abnormal situations (exception callback).

The difference between whenComplete and whenCompleteAsync:

         whenComplete: The thread executing the current task continues to execute the task of whenComplete. 

        whenCompleteAsync: Execution continues to submit the whenCompleteAsync task to the thread pool for execution.

The method does not end with Async, which means that the Action uses the same thread to execute, and Async may use other threads to execute (if it uses the same thread pool, it may also be selected by the same thread for execution)

public static void main(String[] args) throws ExecutionException, InterruptedException {
        System.out.println("Main...Start...."+Thread.currentThread());
        // supplyAsync 可获取返回值
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程" + Thread.currentThread());
            int i = 10 / 0;
            System.out.println("运行结果:" + i);
            return i;
        }, threadPool).whenCompleteAsync((res,exception)->{
            // 当上面的任务执行完成,能得到结果和异常信息,但无法修改返回值
            System.out.println("异步任务完成,结果为:"+res+",异常为:"+exception);
        }).exceptionally(throwable -> {
            // 可以感知异常,同时返回默认值
            return 101;
        });
        Integer integer = result.get();
        System.out.println("Main...End...."+integer);

    }

result:

Main...Start....Thread[main,5,main]
当前线程Thread[pool-1-thread-1,5,main]
异步任务完成,结果为:null,异常为:java.util.concurrent.CompletionException: java.lang.ArithmeticException: / by zero
Main...End....101

Similar to then and reject of Promise in ES6;

3. handle method

public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 方法执行完成后的处理
         */
        System.out.println("Main...Start...."+Thread.currentThread());
        CompletableFuture<Integer> result = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程" + Thread.currentThread());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }, threadPool).handle((res,throwable)->{
            // handle可获取到返回值并可进行修改,且可以感知异常,并修改返回值
            if(res!=null && throwable==null){
                return res;
            }else{
                return 0;
            }
        });
        Integer integer = result.get();
        System.out.println("Main...End...."+integer);

    }

result:

Main...Start....Thread[main,5,main]
当前线程Thread[pool-1-thread-1,5,main]
Main...End....0

4. Thread serialization method

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)

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)


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

thenApply method: When a thread depends on another thread, obtain the result returned by the previous task and return the return value of the current task.

thenAccept method: consume processing results. Receive the processing results of the task and consume the processing without returning any results.

thenRun method: As long as the above task is completed, thenRun will be executed. Only after the task is processed, the subsequent operations of thenRun will be executed.

With Async, execution is asynchronous by default. Same as before.

The above prerequisite tasks must be successfully completed.

Function<? super T,? extends U>

        T: The type of result returned by the previous task

        U: The return value type of the current task

public static void main(String[] args) throws ExecutionException, InterruptedException {
        /**
         * 线程串行化
         *  1、thenRunAsync:无法获取上一步的执行结果,无返回值
         *  .thenRunAsync(() -> {
         *             System.out.println("任务2启动了");
         *         },threadPool)
         *  2、thenAcceptAsync:能获取到上一步的返回值,无返回值
         *  .thenAcceptAsync(res -> {
         *             System.out.println("任务2启动了"+res);
         *         },threadPool)
         *  3、thenApplyAsync:既能获取到上一步的返回值,有返回值
         */
        System.out.println("Main...Start...."+Thread.currentThread());
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("当前线程" + Thread.currentThread());
            int i = 10 / 2;
            System.out.println("运行结果:" + i);
            return i;
        }, threadPool).thenApplyAsync(res->{
            System.out.println("任务2启动了"+res);
            return "hello,"+res;
        },threadPool);
        //future.get() 获取返回值,会阻塞
        System.out.println("Main...End...."+future.get());

    }

result:

Main...Start....Thread[main,5,main]
当前线程Thread[pool-1-thread-1,5,main]
运行结果:5
任务2启动了5
Main...End....hello,5

5. Two task combinations - both must be completed

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)
public <U> CompletableFuture<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action)
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, Executor executor)
public CompletableFuture<Void> runAfterBoth(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor)

Both tasks must be completed to trigger this task.

thenCombine: Combines two futures, obtains the return results of the two futures, and returns the return value of the current task.

 thenAcceptBoth: Combine two futures, obtain the return results of the two future tasks, and then process the tasks without returning a value.

runAfterBoth: Combines two futures. There is no need to obtain the result of the future. It only needs to process the task after the two futures have processed the task.

Example :

First create two tasks

/**
 * 两个都完成
 */
System.out.println("Main...Start...."+Thread.currentThread());
CompletableFuture<Integer> future01 = CompletableFuture.supplyAsync(() -> {
    System.out.println("任务1开始" + Thread.currentThread().getId());
    int i = 10 / 2 ;
    System.out.println("任务1结束");
    return i;
}, threadPool);

CompletableFuture<String> future02 = CompletableFuture.supplyAsync(() -> {
    System.out.println("任务2开始"+Thread.currentThread().getId());
    System.out.println("任务2结束");
    return "Hello";
}, threadPool);

Use runAfterBothAsync (the results of previous task execution cannot be obtained):

future01.runAfterBothAsync(future02,()->{
    System.out.println("任务3启动");
},threadPool);

// 结果:

Main...Start....Thread[main,5,main]
任务1开始12
任务1结束
任务2开始13
任务2结束
任务3启动

Use thenAcceptBothAsync (the results of previous task execution can be obtained, but cannot be returned):

future01.thenAcceptBothAsync(future02,(f1,f2)->{
    System.out.println("任务3开始----f1结果:"+f1+"===f2结果"+f2);
},threadPool);

// 结果:

Main...Start....Thread[main,5,main]
任务1开始12
任务1结束
任务2开始13
任务2结束
任务3开始----f1结果:5===f2结果Hello

Use thenCombineAsync (can obtain the results of previous task execution, modify and return the results):

CompletableFuture<String> future = future01.thenCombineAsync(future02, (f1, f2) -> {
    return f1 +":"+ f2 + "--->Hello";
}, threadPool);
System.out.println("Main...End...."+future.get());

// 结果:
Main...Start....Thread[main,5,main]
任务1开始12
任务1结束
任务2开始13
任务2结束
Main...End....5:Hello--->Hello

6. Two task combination - one completed

public CompletableFuture<Void> runAfterEither(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action)
public CompletableFuture<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor)
public CompletableFuture<Void> acceptEither(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action)
public CompletableFuture<Void> acceptEitherAsync(CompletionStage<? extends T> other, Consumer<? super T> action,Executor executor)
public <U> CompletableFuture<U> applyToEither(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn)
public <U> CompletableFuture<U> applyToEitherAsync(CompletionStage<? extends T> other, Function<? super T, U> fn,Executor executor)

When either future task of the two tasks is completed, the task is executed.

applyToEither: One of the two tasks is completed, obtains its return value, processes the task and has a new return value. acceptEither: One of the two tasks is completed, obtains its return value, and processes the task. There is no new return value. runAfterEither: One of the two tasks is completed. There is no need to obtain the result of the future, process the task, and there is no return value.

7. Multi-tasking combination

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

allOf: wait for all tasks to complete

anyOf: As long as one task is completed

Example:

Create three tasks to simulate data acquisition when users load the homepage

CompletableFuture<String> futureImg = CompletableFuture.supplyAsync(() -> {
    System.out.println("查询商品图片信息");
    return "hello.jpg";
}, threadPool);

CompletableFuture<String> futureAttr = CompletableFuture.supplyAsync(() -> {
    System.out.println("查询商品属性");
    return "白色+1TB";
}, threadPool);

CompletableFuture<String> futureDesc = CompletableFuture.supplyAsync(() -> {
    System.out.println("查询商品介绍");
    return "Apple";
}, threadPool);

Use allOf :

CompletableFuture<Void> allOf = CompletableFuture.allOf(futureImg, futureAttr, futureDesc);

allOf.get(); // 等待所有任务执行完成 返回结果

// 获取所有任务的执行结果
System.out.println("Main...End...."+futureImg.get()+"===>"+futureAttr.get()+"===>"+futureDesc.get());

// 结果:
Main...Start....Thread[main,5,main]
查询商品图片信息
查询商品属性
查询商品介绍
Main...End....hello.jpg===>白色+1TB===>Apple

Use anyOf:

CompletableFuture<Object> anyOf = CompletableFuture.anyOf(futureImg, futureAttr, futureDesc);
anyOf.get(); // 有一个任务执行完成 返回结果

// 获取所有任务的执行结果
System.out.println("Main...End...."+anyOf.get());

// 结果:
Main...Start....Thread[main,5,main]
查询商品图片信息
查询商品属性
Main...End....hello.jpg
查询商品介绍

 

 

Guess you like

Origin blog.csdn.net/weixin_53922163/article/details/128177316