Design and implementation of CompletableFuture callback mechanism

Table of contents

1. Overview of Future Principle and Analysis of Limitations

(1) Review of Future implementation principles

(2) Analysis and recommendation of Future limitations (CompletableFuture)

2. Overview of the CompletableFuture principle and summary of the callback mechanism

(1) Brief description of the core principles of CompletableFuture

(2) Brief description of the design and implementation of the CompletableFuture callback mechanism

3. CompletableFuture callback mechanism design and algorithm implementation

(1) Class diagram analysis

(2) Overall process analysis

(3) Algorithm and implementation

Task notification-postComplete

Push tasks onto the stack-pushStack

Visibility Optimization-lazySetNext

Register and complete callback tasks

Monitor the execution results of multiple Futures-allOf and anyOf

4. Guidance and suggestions during development

References, books and links


Note: If you want to know more about its use, please see: CompletableFuture using Amway and source code analysis_Brief introduction to redis application scenarios_Zhang Yanfeng ZYF's blog-CSDN blog

1. Overview of Future Principle and Analysis of Limitations

(1) Review of Future implementation principles

Future in Java is an asynchronous programming technology that allows us to execute a task in another thread and wait in the main thread to obtain the result after the task is completed. The implementation principle of Future can be understood through two interfaces in Java: Future and FutureTask.

The Future interface is an interface in Java used to represent the results of asynchronous operations. It defines the method get() to obtain the results of asynchronous operations, and you can query whether the operation has been completed through the isDone() method.

The FutureTask class was introduced in Java 5. It is a class that implements the Future and Runnable interfaces. It can encapsulate a task (Runnable or Callable) into an asynchronous operation. The results of task execution can be obtained through the get() method of FutureTask. .

The implementation of FutureTask mainly includes the following steps:

  1. Create a FutureTask object and pass in a task (Runnable or Callable).
  2. Execute the task in another thread and save the task execution results in FutureTask.
  3. Call the FutureTask's get() method in the main thread. If the task has not been completed, the current thread is blocked until the task is completed and the result is returned.

The get() method of FutureTask is a blocking method. If the task has not been completed, the current thread will be blocked until the task is completed. This blocking process can be implemented through a volatile type variable. After the task execution is completed, the done() method will be called to notify the FutureTask that the task has been completed, and the execution result will be set. The done() method will call the callback function of FutureTask and set the execution result to FutureTask after completion.

It should be noted that FutureTask does not guarantee the execution order and execution results of tasks, because task execution is controlled by the thread pool. If you need to ensure the execution order and results of tasks, you can use CompletionService and ExecutorCompletionService.

To sum up, the implementation principle of Future is to encapsulate the task into an asynchronous operation through the Future and FutureTask interfaces, and wait in the main thread to obtain the execution result after the task is completed. FutureTask is a specific implementation of Future, which uses blocking methods and callback functions to obtain the results of asynchronous operations.

(2) Analysis and recommendation of Future limitations (CompletableFuture)

Although Future provides a simple asynchronous programming technology in Java, it also has some limitations, including the following aspects:

  1. Blocking problem : Future's get() method is a blocking method. If the task is not completed, the current thread will always be blocked, which will lead to a decrease in the responsiveness of the entire application.
  2. Unable to cancel the task : The cancel() method of Future can be used to cancel the execution of the task, but if the task has already started execution, it cannot be canceled. At this time, you can only wait for the task to be executed, which will cause a certain performance loss.
  3. Lack of exception handling : Future's get() method will throw an exception, but if an exception is thrown during task execution, Future cannot handle the exception and can only throw the exception to the caller for processing.
  4. Lack of combined operations : Future can only handle a single asynchronous operation and cannot support the combination of multiple operations. For example, you need to wait for multiple tasks to be completed before performing the next operation.

To sum up, although Future provides a simple asynchronous programming technology, its limitations are also obvious. In practical applications, we need to choose appropriate asynchronous programming technology based on specific business needs and performance requirements. For example, CompletableFuture can be used to solve some problems of Future. It can avoid blocking, support exception handling, combined operations and other functions.

2. Overview of the CompletableFuture principle and summary of the callback mechanism

(1) Brief description of the core principles of CompletableFuture

CompletableFuture is a powerful asynchronous programming tool introduced in Java 8. It allows us to handle asynchronous operations in a non-blocking manner and process the results of the asynchronous operations through callback functions.

The core principle of CompletableFuture is implemented based on Java's Future interface and internal state machine . It can achieve asynchronous operation in three steps:

  1.  Create a CompletableFuture object : Through the static factory method of CompletableFuture, we can create a new CompletableFuture object and specify the asynchronous operation of the object. Normally, we can create CompletableFuture objects through the supplyAsync() or runAsync() methods.
  2.  Execution of asynchronous operations : After the CompletableFuture object is created, the asynchronous operations begin to be executed. This asynchronous operation can be a computing task or an IO operation. CompletableFuture will perform this asynchronous operation in another thread so that the main thread will not be blocked.
  3.  Processing of asynchronous operations : After the asynchronous operation is completed, CompletableFuture will modify its internal state based on the execution results and trigger the corresponding callback function. If the asynchronous operation is completed successfully, the completion callback function of CompletableFuture will be triggered; if the asynchronous operation throws an exception, the exception callback function of CompletableFuture will be triggered.

The advantage of CompletableFuture is that it supports chained calls and combined operations . Through the then series methods of CompletableFuture, we can create multiple CompletableFuture objects and connect them in series to form a chained operation flow. In this operation flow, each CompletableFuture object can depend on the previous CompletableFuture object to implement more complex asynchronous operations.

In general, the principle of CompletableFuture is based on Java's Future interface and internal state machine. It can perform asynchronous operations in a non-blocking manner and process the results of the asynchronous operations through callback functions. Through chain calls and combined operations, CompletableFuture can easily implement complex asynchronous programming tasks .

(2) Brief description of the design and implementation of the CompletableFuture callback mechanism

In CompletableFuture, callback is an important mechanism that can automatically trigger the callback function when the asynchronous task is completed .

The callback mechanism of CompletableFuture can be divided into two types: completion callback and exception callback . The completion callback is triggered when the asynchronous task completes successfully, while the exception callback is triggered when the asynchronous task throws an exception. CompletableFuture provides different methods for these two callbacks, allowing you to flexibly set the callback function.

The callback mechanism of CompletableFuture is implemented through Java functional programming . In CompletableFuture, the callback function is a functional interface. For example, the thenAccept method of CompletableFuture requires a Consumer type callback function as a parameter. After the asynchronous task is completed, CompletableFuture will automatically call the callback function and pass the result of the asynchronous task.

In terms of implementation, the callback mechanism of CompletableFuture mainly relies on Java's Future interface and the state machine inside CompletableFuture . When a CompletableFuture is created, its status is incomplete. When the asynchronous task is completed, CompletableFuture will modify the internal state, save the result or exception internally, and then trigger the corresponding callback function.

When the user calls the then series methods of CompletableFuture, CompletableFuture will return a new CompletableFuture object, representing a new asynchronous task. When the original CompletableFuture completes, it automatically triggers the new CompletableFuture's callback function. This chain callback design can easily implement dependencies between multiple asynchronous tasks.

In general, the callback mechanism of CompletableFuture is implemented through Java functional programming and state machines . Through flexible callback function settings and chain calls, CompletableFuture can easily implement asynchronous programming.

3. CompletableFuture callback mechanism design and algorithm implementation

It can be seen from the usage of CompletableFuture that CompletableFuture mainly implements asynchronous programming through callbacks to solve the problem of Future blocking during use .

(1) Class diagram analysis

Its structure is similar to the observer pattern. CompletableFuture is the publisher and uses a linked list to save the observer Completion .

  • The postComplete method of CompletableFuture is a notification method, used to notify observers when CompletableFuture is completed and send subscribed data.
  • The tryFire method of Completion is used to process the results published by CompletableFuture .

If users directly create, register and manage observers, it is not simple enough to use. Therefore, CompletableFuture provides an API that accepts lambda expressions. Observer Completion and its subclasses are defined as internal classes, encapsulating the functional interface corresponding to the lambda expression passed in by the user.

It can be seen from the class diagram that Completion is a chain structure. There are two notification orders for linked lists: first-in-first-out (FIFO) and last-in-first-out (LIFO) . Queue (FIFO) supports higher throughput when there is more competition because different variables are modified when dequeuing and entering the queue. The stack (LIFO) is different from the queue. Pushing and popping modify the same variable, but when popping the stack, the memory of the previously pushed object is likely to remain in the CPU cache. Thread locality is better, and it is suitable for situations with less competition. used in the case. CompletableFuture generally does not compete frequently during use, and the performance of using the last-in-first-out notification sequence is better.

(2) Overall process analysis

In the figure below, stack is the top of the CompletableFuture task stack, Completion is the node of the stack, and its dep field stores the reference to the CompletableFuture that needs to be completed.

The overall process is as follows:

  1. Creation : User creates a CompletableFuture using an API that accepts lambda expressions or creates a CompletableFuture directly and completes it manually.
  2. Registration : Users use the API that accepts lambda expressions to register tasks, monitor the execution results of the created CompletableFuture, and return a new CompletableFuture. The registered task is encapsulated into an observer Completion, and Completion saves a reference to the returned CompletableFuture. Completion is pushed onto the stack when the monitored CompletableFuture is not completed.
  3. Notification : After CompletableFuture is completed, the result is released, the Completion in the notification stack is executed, and the Completion is popped out of the stack.
  4. Completion : After Completion is executed, the referenced CompletableFuture is completed. A completed CompletableFuture notifies its observers.

In the figure below, stack is the top of the CompletableFuture task stack, Completion is the node of the stack, and its dep field stores the reference to the CompletableFuture that needs to be completed.

In the case of chained calls, a simple implementation of notification and completion is to use recursion. Recursion requires a lot of running space. The deeper the recursion level, the more memory it requires. When the call stack space is insufficient, a StackOverflowError will be thrown. Converting recursion into a loop can reduce memory overhead and avoid StackOverflowError. Conversion to recursion can be achieved by merging stacks of multiple CompletableFutures.

(3) Algorithm and implementation

CompletableFuture uses an algorithm called Treiber Stack that uses cas operations to resolve concurrency conflicts and implement non-blocking and lock-free concurrent stacks.

Task notification-postComplete

postComplete implements the logic of task notification and task stack merging. postComplete executes tryFire with the NESTED parameter to tell Completion that it can return the CompletableFuture to be notified.

    final void postComplete() {
        CompletableFuture<?> f = this; Completion h; // f保存的是当前要通知或合并的CompletableFuture
        while ((h = f.stack) != null ||              // f的栈不为空 => (通知未完成 || 合并未完成) => 继续循环 
               (f != this                            // f != this => 在做合并操作;f的栈为空 && 在做合并操作 => f已被合并到this => 看this中还有没有任务可以通知
                && (h = (f = this).stack) != null)) {// this的栈中还有任务可以通知,继续循环
            CompletableFuture<?> d; Completion t;
            if (f.casStack(h, t = h.next)) {         // 出栈,原子操作:栈顶元素stack为h则将stack指向h的下一个元素t,否则返回false,读取最新的栈顶元素重试
                if (t != null) {                     // h不是f的栈中最后一个任务(最后一个直接执行,不用入栈)
                    if (f != this) {                // f != this => 需要做合并操作
                        pushStack(h);                // 将h压入this的栈
                        continue;
                    }
                    h.next = null;                   // detach,帮助垃圾回收
                }
                f = (d = h.tryFire(NESTED)) == null ? this : d;// NESTED:嵌套模式,若tryFire返回的不是null,表示h有要通知的CF,接下来把要通知的CF的栈中任务压入this的栈
            }
        }
    }

Push tasks onto the stack-pushStack

The implementation of node Completion in the stack is as follows. The next field of Completion is used to save the reference of the previous node pushed onto the stack and is a volatile variable. The tryFire method can accept three modes: SYNC, ASYNC and NESTED.

/* ------------- Base Completion classes and operations -------------- */
    @SuppressWarnings("serial")
    abstract static class Completion extends ForkJoinTask<Void>
        implements Runnable, AsynchronousCompletionTask {
        volatile Completion next;      // Treiber stack link
        /**
         * Performs completion action if triggered, returning a
         * dependent that may need propagation, if one exists.
         *
         * @param mode SYNC, ASYNC, or NESTED
         */
        abstract CompletableFuture<?> tryFire(int mode);
        /** Returns true if possibly still triggerable. Used by cleanStack. */
        abstract boolean isLive();
        public final void run()                { tryFire(ASYNC); }
        public final boolean exec()            { tryFire(ASYNC); return true; }
        public final Void getRawResult()       { return null; }
        public final void setRawResult(Void v) {}
    }

The top stack is also a volatile variable and can be updated atomically using cas operations. The implementation of the pop operation is to try to execute casStack in a loop to modify the top of the stack until the modification is successful.

// stack在CompletableFuture对象中的offset
private static final long STACK;
final boolean casStack(Completion cmp, Completion val) {
     // 若stack为cmp,则更新成val,return true;否则,不更新,return false
     return UNSAFE.compareAndSwapObject(this, STACK, cmp, val);
}

The push operation is implemented by trying to modify the next field and the stack field in a loop until the stack field is modified successfully.

/** Returns true if successfully pushed c onto stack. */
final boolean tryPushStack(Completion c) {
    Completion h = stack;
    //c.next=h,延迟写,不保证执行完之后c.next的值立马就对其他线程可见,节省保证可见性的开销
    lazySetNext(c, h);
    //如果成功,则stack修改后的值和c.next的修改后的值都保证对其他线程可见
    return UNSAFE.compareAndSwapObject(this, STACK, h, c);
}
/** Unconditionally pushes c onto stack, retrying if necessary. */
final void pushStack(Completion c) {
    do {} while (!tryPushStack(c));
}

The difference between the push operation and the typical implementation of Treiber Stack is that visibility optimization is done when updating the next value.

From the figure above, you can see that a push operation requires modification of the two volatile variables next and stack. If the stack modification fails, there is no need to ensure the visibility of the next field . Only when the stack is modified successfully does the visibility of c.next need to be guaranteed, so one volatile write operation can be reduced by delayed writing (lasySet).

Visibility Optimization-lazySetNext

Modifying next and modifying stack are used in combination, so when modifying stack, just ensure the visibility of next and stack. putOrderedObject removes the visibility of volatile variable writing operations and only retains its orderliness to process next variables, which can improve the performance of writing next variables.

    static void lazySetNext(Completion c, Completion next) {
        //next是volatile变量,保证有序性和可见性。保证写操作可见性需要将写缓冲(write buffer)刷新到内存,成本较高
        //putOrderedObject只保证有序性语义。
        //下一次写volatile时,写缓冲被刷新到内存,next写入的值保证可见。
        //典型使用场景是将非阻塞数据结构中的节点置空,让节点更快地回收
        UNSAFE.putOrderedObject(c, NEXT, next);
    }

Register and complete callback tasks

Taking thenApply and thenCombine as examples, discuss how CompletableFuture ensures that tasks are executed and only executed once without locking.

Monitor the execution result of a single Future - take thenApply as an example

Constraint: The task can be pushed onto the stack only when the CompletableFuture is not completed.

Reason: If the task is pushed onto the stack after the CompletableFuture callback is completed, the pushed task will not be notified.

Problem: The result may need to be locked.

If the result is not locked, the execution sequence in the figure below may occur under concurrent conditions. When judging whether the result is empty, the CompletableFuture has not yet been completed, and the Completion push operation is performed. However, the push operation is not completed until CompletableFuture is notified, and the task is not notified.

The execution sequence in the case of locking is shown in the figure below, which can solve the above problems:

Lock-free solution: Completion tries to execute again after being pushed onto the stack. If the execution is successful, the task is cleared from the stack.

Monitor the execution results of two Futures - take thenCombine as an example

Assume that CompletableFuture a is executed in thread A and CompletableFuture b is executed in thread B. The logic of thenCombine registration task is as follows:

As you can see from the figure below, if the results of the two Futures are not null at the same time, Completion may be notified by thread A and thread B at the same time and executed twice after registration.

Therefore, a variable needs to be added to mark whether Completion has been executed.

Monitor the execution results of multiple Futures-allOf and anyOf

allOf and anyOf both create CompletableFutures that listen to multiple CompletableFutures. The difference is that

  • The completion condition of allOf is that all monitored Futures are completed and there is no calculation result.
  • The completion condition of anyOf is that any monitored Future is completed, and the calculation result is the calculation result of the earliest completed Future.

4. Guidance and suggestions during development

In actual development, you need to pay attention to the following aspects when using CompletableFuture:

  1. Exception handling: In CompletableFuture, you can use the exceptionally() method or handle() method to handle exceptions during task execution to avoid exceptions causing the entire application to crash. The method of exception handling can be selected according to specific business needs.
  2. Avoid blocking: CompletableFuture provides a series of non-blocking methods, such as thenApplyAsync(), thenAcceptAsync(), thenRunAsync(), etc., which can not block the current thread during task execution, thereby improving the response performance of the entire application.
  3. Combined operations: CompletableFuture supports the combination of multiple operations. You can use the thenCompose() method and thenCombine() method to combine multiple asynchronous operations together to form a task chain, thereby avoiding blocking and sequence issues between tasks.
  4. Use the join() method with caution: In CompletableFuture, the join() method is a blocking method. If the task is not completed, the current thread will always be blocked. Therefore, you need to consider carefully when using the join() method to avoid blocking the entire application.
  5. Reasonable use of thread pools: CompletableFuture uses the ForkJoinPool thread pool by default to execute asynchronous tasks. If there are too many tasks or the task execution time is too long, it may cause thread pool exhaustion and task waiting problems. Therefore, when using CompletableFuture, you need to select an appropriate thread pool based on specific business needs and performance requirements, and tune and manage the thread pool.

To sum up, using CompletableFuture requires a reasonable selection of asynchronous programming technology based on specific business needs and performance requirements, and attention to exception handling, avoiding blocking, reasonable combination of operations, careful use of the join() method, and reasonable use of thread pools.

References, books and links

1. "In-depth analysis of Java concurrent programming: CompletableFuture source code analysis" https://www.jianshu.com/p/02a4d4c4be4d: This article introduces the implementation principles of CompletableFuture in detail through source code analysis, including asynchronous execution, callback functions, and exceptions. processing, etc.

2. "Looking at Concurrent Programming from the Java Concurrency Package (6): Analysis of the CompletableFuture Principle" https://www.cnblogs.com/zhenyulu/p/9267681.html: This article explains the implementation principle of CompletableFuture through code analysis. And discussed the advantages, disadvantages and applicable scenarios of CompletableFuture.

3. "Java 8 Completable Future: Performing Async Tasks" https://www.javacodegeeks.com/2018/05/java-8-completable-future-performing-async-tasks.html: This article is detailed through source code analysis The implementation principle of CompletableFuture is introduced, including thread pool, task execution process, callback function, etc.

4. "Java Concurrent Programming Study Notes - CompletableFuture Source Code Analysis" https://www.cnblogs.com/ll409546297/p/12015692.html: This article introduces the implementation principles of CompletableFuture through source code analysis, including task status and asynchronous Execution, callback functions, exception handling, etc.

5. "In-depth Analysis of CompletableFuture Source Code" https://blog.csdn.net/liuganggang123/article/details/81153429: This article introduces the implementation principles of CompletableFuture in detail through source code analysis, including asynchronous execution, callback functions, and exception handling In other aspects, some issues and considerations of CompletableFuture were also discussed.

6. Wang Peng "Design and Implementation of CompletableFuture Callback Mechanism"

7. Analysis of the principle of CompletableFuture | Old driver teases Java

8. Illustration of CompletableFuture source code_weixin_38592881’s blog-CSDN blog

9. In-depth interpretation of the source code and principles of CompletableFuture_CoderBruis' blog-CSDN blog_future.isdown()

10. CompletableFuture (completable asynchronously executed tasks) source code analysis (the most complete analysis in history for in-depth understanding of concurrency principles)_completablefuture source code analysis_Chen Qing's Blog-CSDN Blog

11. CompletableFuture source code analysis_pngyul’s blog-CSDN blog_tryfire

おすすめ

転載: blog.csdn.net/xiaofeng10330111/article/details/130466437