In depth analysis of Runnable, Callable, FutureTask and completable future principles of concurrency

Posted by dimitar on Tue, 26 Oct 2021 03:10:59 +0200

introduction

About Runnable and Callable interfaces, you may have learned a concept when you first learned Java multithreading programming: there are three ways to create multithreading in Java: Inheriting Thread class, implementing Runnable interface and implementing Callable interface. But in fact, there is only one way to create multithreads: inherit the Thread class, because only new Thread().start() can truly map the execution of an OS kernel Thread. In my opinion, the Runnable and Callable objects created by implementing the Runnable interface and Callable interface can only be called "multithreaded tasks", Because whether it is a Runnable object or a Callable object, the final execution must be executed by the Thread object.

1, Analysis of interface between Runnable and Callable

In Java, the general way to start multithreading is to create a Thread object, as follows:

new Thread(
    @Override
    public void run() {}
).start();

But this method obviously has a fatal defect: when creating an object, the thread executor is bound with the task to be executed, which is not very flexible to use. Therefore, you can create a multithreaded task object by implementing the implementation class of the Runnable interface, or you can directly create an anonymous Runnable internal class object to achieve the same effect, as follows:

public class Task implements Runnable{
    @Override
    public void run() {}
    
    public static void main(String[] args){
        Task task = new Task();    
        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();t2.start();
        
        // Or you can!
        Runnable taskRunnable = new Runnable(){
            @Override
            public void run() {}
        };
        Thread tA = new Thread(task);
        Thread tB = new Thread(task);
        tA.start();tB.start();
    }
}

Generally speaking, the above two methods separate the Thread object of the executor Thread entity from the Runnable object of the task. In the actual coding process, multiple threads can be selected to execute a task task at the same time. This method will make multithreaded programming more flexible.

However, in the actual development process, sometimes a multi-threaded task needs to return a value after execution, but the run() method of the Runnable interface has no return type of void. What should I do when a return value is required? At this point, we can use Callable. Let's take a look at the definition of the Callable interface:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

Obviously, like the Runnable interface, the Callable interface is defined as a functional interface, and like the Runnable interface, only one method call() is provided. Different from Runnable interface:

  • The call() method can have a return value. The return type is generic V, which means that all types of return values are supported.
  • When defining the call() method, it is declared that exceptions can be thrown: throws Exception, but run() cannot.

However, it should be noted that although the Thread class provides many constructor methods, none of them can receive Callable objects, as follows:

public Thread()
public Thread(Runnable target)
Thread(Runnable target, AccessControlContext acc)
public Thread(ThreadGroup group, Runnable target)
public Thread(String name)
public Thread(ThreadGroup group, String name)
public Thread(Runnable target, String name)
public Thread(ThreadGroup group, Runnable target, String name)
public Thread(ThreadGroup group, Runnable target, 
        String name,long stackSize)

As mentioned above, all constructors provided by the Thread class do not provide a constructor that can directly receive Callable objects, so how do you give it to the Thread for execution when using Callable? It must depend on something else before it can be executed by the Thread. What is the thing that Callable depends on? FutureTask! Let's start with the previous case:

public static void main(String[] args) throws Exception {
    FutureTask<String> futureTask = new FutureTask<>(() ->
            Thread.currentThread().getName() + "-Bamboo loves pandas......");
    new Thread(futureTask, "T1").start();
    System.out.println("main Get asynchronous execution result by thread:"
            + futureTask.get());
}

// Execution result: main thread gets asynchronous execution result: T1 - bamboo love panda

Huh? How? Let's take a brief look at the source code (which will be analyzed in detail later):

// FutureTask class
public class FutureTask<V> implements RunnableFuture<V> {
    // FutureTask class → construction method
    public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW; // Set the current task status to NEW
    }
}
// RunnableFuture interface
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

ok ~, so far, we can find that a Callable object can be received in the FutureTask constructor, and FutureTask implements two interfaces: runnable and future. Therefore, when we create a Callable task, we can first encapsulate it into a FutureTask object, and then pass the encapsulated FutureTask to the thread for execution.

2, Detailed explanation of Callable interface and FutureTask structure

Above, we briefly introduce Runnable, Callable interface and FutureTask. Next, we can analyze the specific implementation from the perspective of source code. First, we can briefly look at the general class structure:

// Callable interface
@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

// FutureTask class (other codes will be omitted, which will be described in detail later)
public class FutureTask<V> implements RunnableFuture<V>{}

// RunnableFuture interface
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

// Runnable interface
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

// Future interface
public interface Future<V> {
    // Try to cancel the Callable task. If the cancellation is successful, true will be returned; otherwise, false will be returned
    boolean cancel(boolean mayInterruptIfRunning);
    // Determine whether the Callable task is cancelled
    boolean isCancelled();
    
    // Judge whether the call() execution ends, and the end returns true, otherwise false
    // There are three situations in which true is returned:
    //      ① Results returned after normal execution
    //      ② Exception thrown during execution interrupted execution
    //      ③ The Callable task was cancelled, resulting in the end of execution
    boolean isDone();
    
    // Get the call() execution result, return when the execution is completed, and block until it is completed if it is not completed
    V get() throws InterruptedException, ExecutionException;
    // An upgraded version of the get() method. If it is not completed, it will block until the execution is completed or timeout
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

As mentioned above, the Callable task depends on the FutureTask class, which implements the RunnableFuture interface. In fact, the RunnableFuture interface does not provide new methods, but simply inherits and integrates the runnable and future interfaces. Therefore, the general class diagram relationship is as follows:

In FutureTask, like AQS, there is an int variable state modified with volatile keyword. FutureTask manages the execution state of tasks through it. As follows:

    /*
     * Possible state transitions:
     * NEW -> COMPLETING -> NORMAL
     * NEW -> COMPLETING -> EXCEPTIONAL
     * NEW -> CANCELLED
     * NEW -> INTERRUPTING -> INTERRUPTED
     */
    private volatile int state;
    private static final int NEW          = 0;
    private static final int COMPLETING   = 1;
    private static final int NORMAL       = 2;
    private static final int EXCEPTIONAL  = 3;
    private static final int CANCELLED    = 4;
    private static final int INTERRUPTING = 5;
    private static final int INTERRUPTED  = 6;

There are four state transition changes in state, such as the notes in the source code:

/*
 * Possible state transitions:
 * ①: NEW -> COMPLETING -> NORMAL
 * ②: NEW -> COMPLETING -> EXCEPTIONAL
 * ③: NEW -> CANCELLED
 * ④: NEW -> INTERRUPTING -> INTERRUPTED
 */
  • NEW: initialization status, the status in which the task has just been created
  • COMPLETING: terminate the intermediate state, and the transient state that the task will experience when it changes from NEW to NORMAL/EXCEPTIONAL
  • NORMAL: NORMAL termination status, which is the status after the task is completed normally
  • EXCEPTIONAL: abnormal termination status, which is the status of a task after an exception is thrown during execution
  • Canceled: the status of the task after it is successfully CANCELLED
  • INTERRUPTING: interrupt the intermediate state. The intermediate state in which cancel(true) is called when the task has not been executed or is being executed
  • INTERRUPTED: the final state of the interrupt. The state in which the execution task is cancelled and the execution thread is INTERRUPTED

These are all the states that FutureTask will experience in different situations. When we create a FutureTask, the state of the FutureTask object must be in the NEW state, because this.state = NEW will be executed in the FutureTask construction method; Operation.
At the same time, after the task starts to execute, the state of FutureTask will begin to change. During the two state transitions of NEW → NORMAL and NEW → EXCEPTIONAL, there will also be the intermediate state of COMPLETING, but this intermediate state will exist for a very short time and will immediately change to the corresponding final state. However, it is worth noting that the state transition of FutureTask is irreversible, and at the same time, as long as the state is not in the NEW initialization state, the task can be considered to have ended. For example, the isDone() method of FutureTask to judge whether the task is completed or not:

public boolean isDone() {
    // Returns true as long as the task status is not NEW
    return state != NEW;
}

ok ~, so far, we have briefly understood the class diagram structure and task state of FutureTask. Next, let's take a brief look at the overall member structure of FutureTask. There are two types of threads in FutureTask:
① Performer: the thread that executes asynchronous tasks. There is only one thread
② Waiters: threads waiting to obtain execution results. There may be multiple threads
In the future, we will call these two types of threads "executor" and "waiting person". Let's take a look at the members of FutureTask:

// Execution status of the task
private volatile int state;
// Asynchronous tasks: Callable objects
private Callable<V> callable;
// Task execution result (because it is of Object type, exceptions can also be saved)
private Object outcome;
// Executor thread
private volatile Thread runner;
// Waiting thread: a linked list composed of WaitNode internal classes
private volatile WaitNode waiters;

// Static inner class: WaitNode
static final class WaitNode {
    volatile Thread thread;
    volatile WaitNode next;
    WaitNode() { thread = Thread.currentThread(); }
}

As we mentioned earlier, the get() method of the Future interface is used to obtain the results after asynchronous execution. If the execution is not completed, the wait for the execution results will be blocked. As the implementer of the Future interface, FutureTask naturally implements this method. How does FutureTask block the "waiting person" who wants to get the result when the "performer" has not completed the execution? This is because FutureTask logically has a one-way linked list composed of WaitNode nodes. When a thread attempts to obtain the execution result but the execution has not ended, FutureTask will encapsulate each waiting person into a WaitNode node and add it to the linked list until the executor's task is completed, and then wake up the thread in each node of the linked list. (some are similar to the Condition waiting queue implementation mode of retrantlock)

Because the internal linked list of FutureTask is only a logical linked list, FutureTask itself only stores the head node of the linked list in the member variable waiters, and the other nodes are connected through the next subsequent pointer in WaitNode.

ok ~, so far, we have a basic and comprehensive understanding of the internal structure of FutureTask. Next, let's take a look at the specific implementation process of FutureTask.

3, In depth source code analysis FutureTask execution process and waiting principle

In the previous class diagram structure, we can see that FutureTask implements the RunnableFuture interface, and the RunnableFuture interface inherits the runnable and future interfaces. Next, analyze the implementation of the FutureTask interface from the implementation of the two interfaces.

3.1 analysis on the implementation process of Runnable interface

// Runnable interface
@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Only one run() method is provided in the Runnable interface, so let's first look at the run() implementation in FutureTask:

// FutureTask class → run() method
public void run() {
    // ① Judge whether the state is NEW. If not, it means that the task has been executed or cancelled
    // ② Judge whether there is a thread on the executor position. If there is, it means that the current task is executing
    // If the state is not NEW or the executor is not empty, the execution of the current thread will be terminated directly
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                            null, Thread.currentThread()))
        return;
    
    // If state==NEW and runner==currentThread, continue to execute
    try {
        // Get the asynchronous task to be executed
        Callable<V> c = callable;
        // Check whether the task is empty and check again whether the state is initialized
        if (c != null && state == NEW) {
            // Receive results
            V result; 
            // Receiving termination status: true indicates normal end, and false indicates abnormal end
            boolean ran; 
            try {
                // call() to execute the task and get the execution result
                result = c.call();
                // Change the termination status to normal end
                ran = true;
            } catch (Throwable ex) {
                // The returned result is null
                result = null;
                // Change the termination status to abnormal end
                ran = false;
                // CAS - set the caught exception to the outcome global variable
                setException(ex);
            }
            // If the execution status is normal end
            if (ran)
                // CAS - set the execution result to the outcome global variable
                set(result);
        }
    } finally {
        // Set the reference of the performer thread to null
        runner = null;
        // Check whether the state is INTERRUPTED or INTERRUPTED
        int s = state;
        if (s >= INTERRUPTING)
            // If yes, the response thread is interrupted
            handlePossibleCancellationInterrupt(s);
    }
}

The above is FutureTask's implementation of the run() method. In short, the FutureTask.run() method mainly includes the following four steps:

  • ① Judge the execution status of the task. If it is being executed or has been executed, return directly. Otherwise, continue to execute the task
  • ② If an exception occurs during task execution, setException() is called to write out the captured exception information
  • ③ If the task is executed successfully, get the execution return value and call set() to write out the return value after the task is executed
  • ④ At the end of task execution, judge whether the task status needs to be interrupted. If necessary, call handlePossibleCancellationInterrupt() to handle the interruption

ok ~, let's take a closer look at setException() and set() methods:

// FutureTask class → setException() method
protected void setException(Throwable t) {
    // Use CAS mechanism to change state to COMPLETING intermediate state
    if (UNSAFE.compareAndSwapInt(this,stateOffset,NEW,COMPLETING)) {
        // Write out the caught exception to the outcome member
        outcome = t;
        // Again, use CAS to change the state to EXCEPTIONAL abnormal termination state
        UNSAFE.putOrderedInt(this,stateOffset,EXCEPTIONAL); // Final state
        // Wake up the waiting thread in the waiting queue
        finishCompletion();
    }
}

// FutureTask class → set() method
protected void set(V v) {
    // Use CAS mechanism to modify the state to the intermediate state of COMPLETING
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // Write out the execution results to the outcome member
        outcome = v;
        // Again, use CAS to change the state to NORMAL normal termination state
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // Final state
        // Wake up the waiting thread in the waiting queue
        finishCompletion();
    }
}

As shown in the source code, when an exception is thrown during task execution, the setException() method will be called, and the logic of this method is also divided into four steps:

  • ① First, use the CAS operation to change the state to the intermediate state of COMPLETING
  • ② Write out the caught exception to the outcome member
  • ③ After writing out the caught exception, use CAS again to change the state to EXCEPTIONAL exception termination state
  • ④ Call the finishcompletement () method to wake up the waiting thread in the waiting list

However, after the normal execution of the task, the set() method will be called, and the set() method is the same as the setException() method, which is also divided into four steps:

  • ① First, use the CAS operation to change the state to the intermediate state of COMPLETING
  • ② Write out the return value at the end of normal task execution to the outcome member
  • ③ After writing out, use CAS again to change the state to NORMAL normal termination state
  • ④ Call the finishcompletement () method to wake up the waiting thread in the waiting list

ok ~, then let's take a look at the finishcompletement() method of waking up the "waiting" thread in the waiting list:

// FutureTask class → finishcompletement() method
private void finishCompletion() {
// The state must be the final state before calling this method
    // Get the head node saved in waiters, and traverse the whole logical linked list according to the head
    for (WaitNode q; (q = waiters) != null;) {
        // Use cas operation to set the original head node to null
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                // Get q node's thread
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null; // Null thread reference information
                    LockSupport.unpark(t);// Wake up the thread in the node
                }
                // Get the next node in the linked list
                WaitNode next = q.next;
                // If the next node is empty, it means it is already at the end of the linked list
                if (next == null)
                    // Then the loop is terminated
                    break;
                // Empty the reference information of subsequent nodes of the current node to facilitate GC
                q.next = null; 
                // Assign the obtained successor node to q
                q = next;
            }
            // Exit the loop after traversing the entire linked list
            break;
        }
    }
    // The done() method has no concrete implementation and is left to the user for expansion
    // According to the requirements, the executor thread can do more aftercare work before the execution is completed
    done();
    callable = null;        // to reduce footprint
}

The logic of the finishcompletement () method is relatively simple. Through the head node saved in the member variable waiters and the pointer to the next successor node, traverse and wake up the waiting thread in all nodes of the whole logical linked list.

So far, the logic of task execution has been analyzed, but at the end of FutureTask.run(), there is a finally statement block, which represents the logic that will be executed regardless of whether there are exceptions during task execution, as follows:

finally {
    // Set the reference of the performer thread to null
    runner = null;
    // Check whether the state is INTERRUPTED or INTERRUPTED
    int s = state;
    if (s >= INTERRUPTING)
        // If yes, the response thread is interrupted
        handlePossibleCancellationInterrupt(s);
}

// FutureTask class → handlePossibleCancellationInterrupt() method
private void handlePossibleCancellationInterrupt(int s) {
    // 1. If state==INTERRUPTING, interrupt the intermediate state
    if (s == INTERRUPTING)
        // 3. If the thread obtains cpu resources from the ready state again, it returns to the execution state
        //   The loop calls the yield() method to keep the current thread in a ready state
        //   Until the thread is interrupted and state==INTERRUPTED
        while (state == INTERRUPTING)
            // 2. Call yield() to let the current thread give up cpu resources and exit the execution state
            //   Return to the ready state to respond to thread interrupts
            Thread.yield(); 
}

In the finally statement block, it will judge whether the state is INTERRUPTED or INTERRUPTED. If so, it will call the handlePossibleCancellationInterrupt() method to respond to the thread interrupt operation. For the specific logic of this method, please refer to the notes I wrote in the source code above. The overall implementation process is as follows:

At this point, the process analysis of the whole FutureTask execution task is completed.

3.2. Implementation analysis of FutureTask on Future interface

As mentioned earlier, FutureTask implements the RunnableFuture interface, and RunnableFuture inherits the Future interface in addition to Runnable. Next, let's take a look at the definition of the Future interface:

// Future interface
public interface Future<V> {
    // Try to cancel the Callable task. If the cancellation is successful, true will be returned; otherwise, false will be returned
    boolean cancel(boolean mayInterruptIfRunning);
    // Determine whether the Callable task is cancelled
    boolean isCancelled();
    
    // Judge whether the call() execution ends, and the end returns true, otherwise false
    // There are three situations in which true is returned:
    //      ① Results returned after normal execution
    //      ② Exception thrown during execution interrupted execution
    //      ③ The Callable task was cancelled, resulting in the end of execution
    boolean isDone();
    
    // Get the call() execution result, return when the execution is completed, and block until it is completed if it is not completed
    V get() throws InterruptedException, ExecutionException;
    // An upgraded version of the get() method. If it is not completed, it will block until the execution is completed or timeout
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

The Future interface mainly provides two types of methods: the get method to obtain the task execution result and the cancel method to cancel the task. We analyze them step by step from get - > cancel. Let's take a look at the implementation of get method by FutureTask class:

// FutureTask class → get() method
public V get() throws InterruptedException, ExecutionException {
    // Get task status
    int s = state;
    // If the task state is not greater than the termination intermediate state, the thread is blocked
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // If the state is greater than the termination intermediate state, it means that it is the final state, and the execution result is returned
    return report(s);
}

// FutureTask class → timeout version get() method
public V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException {
    if (unit == null)
        throw new NullPointerException();
    int s = state;
    // If the task is not completed, call awaitDone to block the thread at a given time
    // If the time reaches the status or is not greater than COMPLETING, it means that the task has not been completed
    // Then, a TimeoutException is thrown to forcibly interrupt the execution of the current method and exit the waiting state
    if (s <= COMPLETING &&
        (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
        throw new TimeoutException();
    // If the task execution is completed or within a given time, the execution result is returned
    return report();
}

The logic of FutureTask.get() method is relatively simple:

  • ① First, judge whether the task has been completed (whether state < = completing, if < = means it has not been completed)
  • 2. If the task is not executed or is still executing, call awaitDone() method to block the current waiting thread.
  • ③ If the task has been executed (the status changes to the final status), call report() to return the execution result

The FutureTask.get() method of the timeout version is somewhat inconsistent in the second step. In the get method of the timeout version, when the task has not been executed or is still in the process of execution, it will call the awaitDone(true,unit.toNanos(timeout))) method to wait within a given time, If the time is up and the task is not finished, a TimeoutException exception will be thrown to force the thread to exit the waiting state.

ok ~, then look at the awaitDone() and report() methods:

// FutureTask class → awaitDone() method
private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
    // Calculate the waiting deadline
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    boolean queued = false;
    for (;;) {
        // If the thread interrupt message appears, the waiting list node information is removed
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        
        // Get the task status again. If it is greater than COMPLETING
        // It means that the task has been executed, and the latest state value is returned directly
        int s = state;
        if (s > COMPLETING) {
            if (q != null)
                q.thread = null;
            return s;
        }
        // If state = = intermediate status, it means that the task is almost completed
        // Then let the current thread give up cpu resources to enter the ready state and wait a little
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        // If the task is still executing or not, build the waitnode node
        else if (q == null)
            q = new WaitNode();
        // The constructed waitnode node is added to the logical linked list by cas mechanism
        // Note: the linked list is a stack structure, so it is not a new node
        // It becomes the next node of the previous node, but the new node becomes the head node
        // The old node becomes the next successor node (which is convenient for maintaining the logical linked list structure)
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        // If it is a timeout waiting mode, first judge whether the time has timed out
        // If it has timed out, delete the corresponding node and return the latest state value
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            // If it has not timed out, suspend the thread for a specified time
            LockSupport.parkNanos(this, nanos);
        }
        else
            // If it is not a timeout version, the blocking thread is suspended directly
            LockSupport.park(this);
    }
}

// FutureTask class → report() method
private V report(int s) throws ExecutionException {
    // Gets the value of the member variable outcome
    Object x = outcome;
    // If state is in normal termination status, the execution result is returned
    if (s == NORMAL)
        return (V)x;
    // If the task is cancelled or the thread is interrupted, a cancelationexception is thrown
    if (s >= CANCELLED)
        throw new CancellationException();
    // If state is in the abnormal termination state, the captured exception information will be thrown
    throw new ExecutionException((Throwable)x);
}

ok ~, the source code is as above. Let's take a look at the slightly complex awaitDone() method. The awaitDone() method will first calculate the waiting deadline (there is no deadline if overtime waiting is not required). After calculating the waiting deadline, an endless loop will be opened. In the loop, the following steps will be executed each time:

  • ① Judge whether the current waiting thread is interrupted by other threads. If so, remove the corresponding node information and throw an InterruptedException exception to force the interrupt execution
  • ② Judge whether the state is greater than COMPLETING. If it is greater than, the task execution ends. Empty the thread information of the current node and return the latest state value
  • ③ Judge whether state is equal to COMPLETING. If it is equal to, it means that the execution is about to end. The current thread gives up CPU resources, exits the execution state and enters the ready state, waiting for the final state to appear
  • ④ If the task is not finished, judge whether the current waiting thread has built a waitnode node. If not, build node information for the current thread
  • ⑤ If the newly constructed node has not been added to the waiting list, the cas mechanism is used to set the constructed node as the head node, and the old head becomes the next node of the current node
  • ⑥ Use the LockSupport class to suspend the blocking of the current thread. If it is a get() waiting for a timeout, suspend the blocking for a specific time

The above is the execution process of the whole awaitDone() method. Understand the above example:

Suppose that the current FutureTask status is NEW, and the member waiter = null (equivalent to that the linked list is empty and head=null). At this time, there are two threads: T1 first calls get() to obtain the execution result, and then T2 also calls get() to try to obtain the execution result. At the same time, the task has not started or is still in the process of execution, then the execution processes of these two threads are as follows:

  • T1: in the first cycle, q==null and! If the queued condition holds, build node information for T1, and use cas mechanism to set waiter = T1, and T1 node becomes head node. The first cycle ends
  • T2: in the first cycle, q==null and! If the queued condition holds, construct the node information for T2, and use the cas mechanism to make waiter=T2,next=T1,T2 become the head node, T1 become the successor node, and the first cycle ends
  • T1: in the second cycle, assuming that the task is still executing or has not been executed, all conditions are not true, only the last blocking condition is true, blocking T1 thread, and T1 hangs until the end of execution
  • T2: in the second cycle, it is assumed that the task is still executing or has not been executed, and all conditions are not true. Only the last blocking condition is true, blocking the T2 thread, and T2 hangs until the end of execution
  • T1: after the end of the task execution, call finishCompletion() to wake up the thread in the linked list, T1 resume execution, the third cycle starts, state>COMPLETING is established, clears the node thread information and returns the latest state value.
  • T2: after the end of the task execution, call finishCompletion() to wake up the thread in the linked list, T2 resume execution, the third cycle starts, state>COMPLETING is established, clears the node thread information and returns the latest state value.

ok ~, this is the end of the get method analysis of the Future interface. Next, let's continue to look at the cancel() method:

// FutureTask class → cancel() method
// If the input parameter is true, it means to interrupt the execution of the executor thread
// If the input parameter is false, the task execution will be cancelled
public boolean cancel(boolean mayInterruptIfRunning) {
    // If state= New means that the task has been executed and returns false directly
    // If the input parameter is false, CAS can modify state=CANCELLED and return true
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    // Because an exception may be thrown during the execution of the call() method
    // Therefore, the try finally statement block is required to ensure that the waiting thread wakes up
    try {
        // If the input parameter is true, the performer thread is interrupted
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    // interrupt() does not guarantee that the execution thread will be interrupted
                    // java deprecates the stop() method because it is unsafe to force a thread break
                    // Instead, it is a coordinated interrupt. After the thread calls interrupt()
                    // Only an interrupt signal is sent, and the interrupted thread decides not to respond to the interrupt operation
                    t.interrupt();
            } finally { // final state
                // After interruption, use CAS to modify the final state of state=INTERRUPTED
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        // Whether the task is cancelled or the execution is interrupted, all threads waiting for the linked list will wake up after completion
        finishCompletion();
    }
    return true;
}

When futureask. Cancel() method is called for an item, it will decide whether to interrupt the executor's thread or cancel the task execution according to the passed boolean value. The specific execution logic is as follows:

  • When mayInterruptIfRunning passes in true:
    • ① Judge whether the task has been executed. If yes, it will directly return false. Otherwise, it will interrupt the intermediate state with state=INTERRUPTING using cas mechanism
    • ② Get the executor thread runner member, call the executor's interrupt() method, and send an interrupt signal to the executor thread
    • ③ Use CAS mechanism to interrupt state=INTERRUPTED to the final state
    • Call the finishcompletement () method to wake up all the waiting threads in the linked list
  • When mayInterruptIfRunning passes in false:
    • ① Judge whether the task has been executed. If yes, return false directly
    • ② If the task has not been executed or is still executing, use the cas mechanism to cancel the status of state=CANCELLED

However, it is not difficult to find that in the previous source code comments, I wrote that the t.interrupt() method cannot forcibly interrupt Thread execution. The specific reason is not that it is impossible to forcibly interrupt. This can be realized. In the earlier versions of JDK, Thread also provided a stop() method to forcibly stop Thread execution, However, because forcibly closing the execution Thread will lead to a series of problems, such as security problems and data consistency problems, the method of forcibly interrupting the Thread such as stop() is abandoned in subsequent versions of JDK, and the method of coordinated interruption such as interrupt() is replaced. When a Thread calls the interrupt() method of another Thread, it will not directly force the Thread to be interrupted, but only send an interrupt signal to the Thread to be interrupted. Whether to interrupt execution is determined by the execution Thread itself, and whether the execution Thread can detect the interrupt signal depends on the code running by the execution Thread.

ok ~, who might wonder: if the executor thread cannot be guaranteed to be interrupted, is the cancel() method still effective?
The answer is definitely useful, because after calling the cancel() method, as long as the task is not finished, the state will be modified to the CANCELLED or INTERRUPTING state. Then the subsequent waiting thread will detect that the task state is not NEW when executing get, and the awaitDone() method will not block the wait.

So far, the analysis of the two core methods of the Future interface has been completed. Next, let's take a look at the two methods that determine the type of is: isCancelled(), isDone():

// FutureTask class → isCancelled() method
public boolean isCancelled() {
    // If state > = cancel led, it indicates that the state is not the execution termination state
    // Then it means that it must be interrupted or cancelled, so:
    // State > = cancelled means that the task is cancelled, otherwise it is not
    return state >= CANCELLED;
}

// FutureTask class → isDone() method
public boolean isDone() {
    // If state= The new initialization status represents the end of task execution
    // Because even the intermediate state will soon become the final state
    return state != NEW;
}

Finally, a simple FutureTask.get() execution process is shown below:

3.3 FutureTask summary

Before the advent of FutureTask, multithreaded programming in Java could not obtain the execution results after executing tasks. When we need the results after multithreading execution, we need to go through complex implementation (such as writing to the cache or reading from the main thread in the global variable). FutureTask integrates three interfaces: Runnable, Callable and Future, so that our multithreaded tasks can obtain the multithreaded execution results asynchronously. FutureTask will save the result after execution in the member variable: outcome. The thread waiting to get the execution result can read the value of the outcome member.

4, Detailed explanation of completabilefuture class

In the previous FutureTask, if you want to obtain the results of multi-threaded execution, there are two methods: one is to poll FutureTask.isDone() method to obtain the execution results when the result is true, and the other is to call FutureTask.get() method. However, neither method can realize asynchronous callback in the real sense. Because task execution takes time, the main thread will be forced to block and wait for the execution result to return before proceeding. At most, the waiting time can only be reduced to a certain extent, such as:

public static void main(String[] args) throws Exception {
    FutureTask<String> futureTask = new FutureTask<>(() ->
            Thread.currentThread().getName() + "-Bamboo loves pandas......");
    new Thread(futureTask, "T1").start();
    
    // You can finish other work here first, because it takes time to execute the task
    
    // Finally, get the execution results
    System.out.println("main Get asynchronous execution result by thread:"
            + futureTask.get());
}

This method can use the execution time of asynchronous tasks to complete other tasks to a certain extent, but in general, it is different from the original intention of the design of "obtaining execution results asynchronously". The emergence of completable future can realize the real sense of asynchrony. It will not block the thread obtaining the execution result because the task has not been completed. Completable future also puts the process of processing the execution result into the asynchronous thread, and uses the concept of callback function to solve the problem.

Completable Future is a class introduced after JDK8 supports functional programming. It implements the interface between Future and CompletionStage, uses the methods provided in the CompletionStage interface to support the functions and operations triggered when the task is completed, and uses then, when and other operations to prevent get blocking of FutureTask and polling isDone. However, because the completabilefuture class is newly added to JDK8, it will cooperate with a large number of JDK1.8 features such as function programming, chain programming and Lambda expression. Therefore, if you don't know much about JDK1.8 features, you can refer to the previous article: New Java 8 features.

Because there are about fifty or sixty methods provided in the completabilefuture class, and there are also many internal classes, we will analyze some important methods one by one this time, first make a simple understanding, and then gradually analyze the principle and implementation.

4.1. Creation method of completable future asynchronous task

There are three ways to create an asynchronous task in completable future:

  • ① Using the same method as the previous FutureTask, the creation is completed through the new object
  • ② The creation is completed through the static method provided by completable future
  • ③ The creation is completed through the member method provided by completabilefuture

Here's a step-by-step demonstration:

4.1.1 create asynchronous tasks by creating completabilefuture objects

Because the completable future class is used as an optimized version of FutureTask, in addition to the writing method of Java 8 syntax, it also retains the usage of FutureTask, as follows:

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        CompletableFuture completableFuture = new CompletableFuture();
        new Thread(()->{
            System.out.println("Asynchronous task......");
            // After execution, you can write out the return value to the completable future object
            completableFuture.complete(Thread.currentThread().getName());
        }).start();
        // The main thread obtains the asynchronous task execution result
        System.out.println("main Thread get execution result:" + completableFuture.get());
        for (int i = 1; i <= 3; i++){
            System.out.println("main thread  - Output:"+i);
        }
        
    }
   /**
     *  Execution results:
     *     Asynchronous tasks
     *     main Thread get execution result: Thread-0
     *     main Thread - output: 1
     *     main Thread - output: 2
     *     main Thread - output: 3
     */
}

This method is relatively simple and easy to understand. Create a thread to perform asynchronous operation. After execution, write the value to be returned to the completable future object, and the main thread calls the completable future. Get () method to obtain the value written back by the asynchronous thread. Obviously, this method is no different from the previous FutureTask. Before the main thread obtains the execution result, because the task is still executing, the main thread will be forced to block and wait until the task execution is completed.

4.1.2. Complete asynchronous task creation through the completable future static method

Completabilefuture class provides five static methods to complete the operation of creating asynchronous tasks, as follows:

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);
public static <U> CompletableFuture<U> completedFuture(U value);

Among the four methods, the method beginning with run represents the creation of an asynchronous task without return value, and the method beginning with supply represents the creation of an asynchronous task with return value. At the same time, both methods support specifying the execution thread pool. If the execution thread pool is not specified, completable future will use the threads in the ForkJoinPool.commonPool() thread pool to execute the created asynchronous tasks by default. ok ~, let's understand the previous case:

Create an asynchronous task, complete the even sum within 100, and return the sum result after execution. The code is as follows:

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        // Create asynchronous task with return value:: syntax for method reference
        CompletableFuture<String> supplyCF = CompletableFuture
                .supplyAsync(CompletableFutureDemo::evenNumbersSum);
        // Execute successful callback
        supplyCF.thenAccept(System.out::println);
        // Callback with exception during execution
        supplyCF.exceptionally((e)->{
            e.printStackTrace();
            return "Exception occurred during asynchronous task execution....";
        });
        // The main thread performs the print 1234... Operation
        // Because if the thread pool execution task is not specified for completable future,
        // Completable future is the thread using ForkJoinPool.commonPool() by default
        // At the same time, it is performed as the guardian thread of the main thread. If the main hangs, the asynchronous task is executed
        // The thread of the task will also terminate and end, and will not continue to execute asynchronous tasks
        for (int i = 1; i <= 10; i++){
            System.out.println("main thread  - Output:"+i);
            Thread.sleep(50);
        }
    }
    // Sum even numbers within 100
    private static String evenNumbersSum() {
        int sum = 0;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) sum += i;
        }
        return Thread.currentThread().getName()+"thread  - 100 Sum of inner even numbers:"+sum;
    }
   /**
     * Execution results:
     *      main Thread - output: 1
     *      main Thread - output: 2
     *      ForkJoinPool.commonPool-worker-1 Thread - sum of even numbers within 100: 2550
     *      main Thread - output: 3
     *      main Thread - output: 4
     *      main Thread - output: 5
     *      main Thread - output: 6
     *      main Thread - output: 7
     *      main Thread - output: 8
     *      main Thread - output: 9
     *      main Thread - output: 10
     **/
    
}

In the above case, through CompletableFuture.supplyAsync, an asynchronous task supplyCF with return value is created. Because no thread pool is specified, the default ForkJoinPool.commonPool() thread pool is used to complete the execution of the task, and supplyCF.thenAccept is used as a successful callback method, supplyCF.exceptionally is used as the callback method when exceptions are thrown during execution. At the same time, after the main thread main creates and completes the asynchronous task, it continues to print the logic of 1, 2, 3, 4... After writing the successful and failed callback functions. From the above execution results, we can see that after the main thread has created the asynchronous task and related subsequent processing, it does not block and wait for the completion of the task, but continues to execute the next logic. When the task execution ends, the callback function defined in advance will be triggered to return the task execution result (if an exception occurs during execution, the captured exception information will be returned to the exceptionally callback function). Obviously, compared with the previous FutureTask, the completable future task realizes "asynchrony" in execution and return of execution results.

ok ~, next let's look at several other static methods for creating completable future asynchronous tasks. They are as follows:

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        // Create an asynchronous task with a return value
        CompletableFuture<String> supplyCF = CompletableFuture
                .supplyAsync(CompletableFutureDemo::evenNumbersSum);
        // Execute successful callback
        supplyCF.thenAccept(System.out::println);
        // Callback with exception during execution
        supplyCF.exceptionally((e)->{
            e.printStackTrace();
            return "Exception occurred during asynchronous task execution....";
        });
        // The main thread performs the print 1234... Operation
        // Because if the thread pool execution task is not specified for completable future,
        // Completable future is the thread using ForkJoinPool.commonPool() by default
        // At the same time, it is performed as the guardian thread of the main thread. If the main hangs, the asynchronous task is executed
        // The thread of the task will also terminate and end, and will not continue to execute asynchronous tasks
        for (int i = 1; i <= 10; i++){
            System.out.println("main thread  - Output:"+i);
            Thread.sleep(50);
        }
        
        /***************************************************/
        
        // Create an asynchronous task, and the return value has been given
        CompletableFuture c = CompletableFuture.completedFuture("Bamboo");
        c.thenApply(r -> {
            System.out.println("Last task result:"+r);
            return r+"...panda";
        });
        c.thenAccept(System.out::println);
        
        /***************************************************/

        // Create an asynchronous task with no return value
        CompletableFuture runCF = CompletableFuture.runAsync(()->{
            System.out.println(Thread.currentThread().getName()+"Asynchronous task with no return value");
        });
        
        /***************************************************/
        
        // Create a singleton thread pool
        ExecutorService executor = Executors.newSingleThreadExecutor();
        // Create an asynchronous task with a return value and specify the thread pool to execute
        CompletableFuture<String> supplyCFThreadPool =
                CompletableFuture.supplyAsync(CompletableFutureDemo::oddNumbersSum,executor);
        // //Callback with exception during execution
        supplyCFThreadPool.thenAccept(System.out::println);
        // Callback with exception during execution
        supplyCF.exceptionally((e)->{
            e.printStackTrace();
            return "Exception occurred during asynchronous task execution....";
        });
        
        // Close thread pool
        executor.shutdown();
    }

    // Sum even numbers within 100
    private static String evenNumbersSum() {
        int sum = 0;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) sum += i;
        }
        return Thread.currentThread().getName()+"thread  - 100 Sum of inner even numbers:"+sum;
    }
    
    // Sum odd numbers within 100
    private static String oddNumbersSum() {
        int sum = 0;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) sum += i;
        }
        return Thread.currentThread().getName()+"thread  - 100 Sum of inner odd numbers:"+sum;
    }
}

In the above cases, four asynchronous tasks are created respectively. The first is the case analyzed above, which will not be repeated.

  • The second is to create an asynchronous task with a return value. At the same time, different from the first task: we specify the thread pool executor for execution, then the default ForkJoinPool.commonPool() thread pool will not be used when the task is executed. However, when using this method, you must remember to close the thread pool you created.
  • The third asynchronous task creates an asynchronous task with no return value through the completabilefuture.runasync method. The parameter passed is a Runnable object, which is not different from the initial new Thread() method, but the difference is that the execution of the task is also executed through the default ForkJoinPool.commonPool() thread pool because there is no thread pool specified, It will not be executed by another thread.
  • The fourth task is to create a completable future task with a return value specified in advance. Many people may find this method very weak, but you can complete the chain creation with the completable future member method.

4.1.3. Complete asynchronous task creation through completabilefuture member method

The premise of creating a task in this way is to build on the fact that a completable future object has been created. Generally speaking, this kind of member method creates asynchronous tasks in the form of serialization. This method can be used when the next task depends on the execution result of the previous task. There are many such methods available in completable future:

// You can then create a return task based on the completabilefuture object
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)

// You can continue to execute when the previous task fails
public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, 
                                    ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, 
                                    ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, 
                                    ? extends U> fn,Executor executor);

// You can then create a no return task based on the completabilefuture object
CompletionStage<Void> thenRun(Runnable action); 
CompletionStage<Void> thenRunAsync(Runnable action); 
CompletionStage<Void> thenRunAsync(Runnable action,Executor executor); 

// It is similar to the thenApply method, but the thenApply method operates on the same completable future
// This kind of method is to produce a new completable future < return value > object for operation
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)

Some common methods are listed above. The methods are generally divided into four categories. These four types of methods can make the task execute serially:

  • ① thenApply class: this method can create a new returned task based on the previous task.
  • ② Handle class: it has the same function as theapply class, except that theapply class method can only be executed when the previous task is executed normally. When the previous task throws an exception, it will not be executed. The handle class can also be executed in case of exception in the previous task.
  • ③ thenRun class: this method can create a new non return task based on the previous task.
  • ④ Thenpose class: it is roughly the same as thenApply class, except that each downward pass is a new completable future object, while thenApply passes down the same completable future object

However, it is not difficult to find that no matter what kind of method, in fact, the method name will be followed by Async and not followed by Async. What does this mean by Async? If a method without Async is called to create a task, it is executed by the execution thread of the previous task. If the previous task is not completed, the currently created task will wait for the execution of the previous task. However, tasks created through Async methods are not subject to this restriction. For tasks created by calling methods with Async method name, the specific execution thread will be determined according to the actual situation, which can be divided into the following three cases:

  • ① If the previous task has been executed, the currently created task will be handed over to the execution thread of the previous task for execution
  • ② If the previous task has not finished executing, another thread will be started to execute
  • ③ If an execution thread pool is specified when creating a task, the threads of the specified thread pool will be used for execution

ok ~, after having a basic understanding of these methods, let's look at a case:

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        CompletableFuture cf =
                CompletableFuture.supplyAsync(CompletableFutureDemo::evenNumbersSum)
                    // Chain programming: continue to execute new tasks based on the return of the previous task
                    .thenApply(r -> {
                        System.out.println("Get the execution results of the previous task:" + r);
                        // Complete the calculation through the execution result of the previous task: sum all numbers of 100
                        return r + oddNumbersSum();
                    }).thenApplyAsync(r -> {
                        System.out.println("Get the execution results of the previous task:" + r);
                        Integer i = r / 0; // Throw exception
                        return r;
                    }).handle((param, throwable) -> {
                        if (throwable == null) {
                            return param * 2;
                        }
                        // Get caught exception
                        System.out.println(throwable.getMessage());
                        System.out.println("I can be on the last mission" +
                                "Execute when an exception is thrown....");
                        return -1;
                    }).thenCompose(x -> 
                        CompletableFuture.supplyAsync(() -> x+1
                    )).thenRun(() -> {
                        System.out.println("I am a serial no return task....");
                });

        // The main thread performs hibernation for a period of time
        // Because if the thread pool execution task is not specified for completable future,
        // Completable future is the thread using ForkJoinPool.commonPool() by default
        // At the same time, it is performed as the guardian thread of the main thread. If the main hangs, the asynchronous task is executed
        // The thread of the task will also terminate and end, and will not continue to execute asynchronous tasks
        Thread.sleep(2000);
    }

    // Sum even numbers within 100
    private static int evenNumbersSum() {
        int sum = 0;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) sum += i;
        }
        return sum;
    }

    // Sum odd numbers within 100
    private static int oddNumbersSum() {
        int sum = 0;
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) sum += i;
        }
        return sum;
    }
}

In this case, we created six asynchronous tasks:

  • ① Sum all even numbers within 100
  • ② Calculate the sum of all numbers in 100 based on the result of the first task plus the total value of odd numbers in 100
  • ③ An exception is thrown based on the result of the second task divided by 0
  • ④ Use handle to create a task that can still be executed when the previous task throws an exception
  • ⑤ Use thenpose to create a task based on the return value of the previous task + 1
  • ⑥ Created a task with no return value using thenRun

The results are as follows:

/* *
 * Execution results:
 *      Get the execution result of the previous task: 2550
 *      Get the execution result of the last task: 5050
 *      java.lang.ArithmeticException: / by zero
 *      I can still execute when the last task threw an exception
 *      I am a serial no return task
 * */

So far, the three ways to create completable future asynchronous tasks have been introduced. Next, let's look at some other common operations in completable future.

4.2. Acquisition and callback processing of execution results of completable future asynchronous tasks

In the previous FutureTask, the execution results are obtained through FutureTask.get(). In completable future, various methods are provided, which can be obtained in a blocking manner with the previous FutureTask or notified asynchronously through a callback function. Next, you can take a look at some related methods provided in completable future:

// Block the main thread to get the execution result (consistent with FutureTask.get())
public T get();
// Timeout version of last method
public T get(long timeout,TimeUnit unit);
// Try to get the execution result, return the execution result when the execution is completed, and return the task parameters when the execution is not completed
public T getNow(T valueIfAbsent);
// Block the main thread and wait for the end of task execution
public T join();
// This method can be used to write out the return value after the normal execution of asynchronous tasks without return
public boolean complete(T value);
// After no returned asynchronous task executes exception, you can write out the captured exception information through this method
public boolean completeExceptionally(Throwable ex);

// Callback function after normal task execution: the thread executing the task executes the callback logic by default
public CompletionStage<Void> thenAccept(Consumer<? super T> action);
// Callback function after normal task execution: start another thread to execute callback logic
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
// Callback function after normal task execution: Specifies the thread pool to execute callback logic
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,
                                Executor executor);
// Callback method executed when an exception occurs during execution
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
                                
// Callback logic to be executed after execution
public CompletableFuture<T> whenComplete(BiConsumer<? super T,
                            ? super Throwable> action)
// Asynchronous version of last method
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,
                            ? super Throwable> action)
// The specified thread pool version of the previous method
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,
                            ? super Throwable> action, Executor executor)

The methods listed above can be divided into the following categories:

  • ① The main thread calls the get and getNow methods that directly obtain the execution results
  • ② You can write out the execution result for asynchronous tasks with no return value: methods beginning with complete
  • ② Successful callback of normal task execution: method starting with thenAccept
  • ④ Callback for task execution throwing exception: exceptionally method
  • ⑤ Callback at the end of task execution: the method starting with whenComplete

ok ~, these methods are relatively simple to use, so we won't use the case demonstration. Because this chapter is long enough and there are still contents to be discussed below, those who are interested can write a simple Demo debugging by themselves.

4.3. Completion stage task timing relationship: serial, parallel and aggregation implementation

Completabilefuture implements the CompletionStage and Future interfaces at the same time, and some methods are provided in the CompletionStage interface, which can well describe the serial, parallel, aggregation and other timing relationships between tasks.

4.3.1 timing relationship between CompletionStage tasks - Serial

In the implementation of the completabilefuture class, there are always five categories of methods used to describe the serial relationship between tasks: thenApply, thenAccept, thenRun, handle and thencomposite. In the completabilefuture class, the functions whose method names begin with these five functions are used to describe the serial relationship between tasks.

Interpretation of completable future serial: for example, three tasks ① ② ③ are created at present. These three tasks are executed asynchronously, but task ② depends on the execution result of ①, task ③ depends on the execution result of ②, and the execution sequence is always ① → ② → ③. This relationship is called serial relationship.

ok~, for the case of serial, refer to 4.1.3. Complete the asynchronous task creation section through the completable future member method.

4.3.2 timing relationship between CompletionStage tasks - parallel

In the completion stage, there is no method to describe the parallel relationship between tasks, because it is not necessary. After all, parallel is only serial multiple execution.

Some people may not understand my last sentence above: "after all, parallelism is only serial multi execution". Let's take an example to understand: for example, the main thread creates three tasks ① ② ③, all of which are executed by the T1 thread, then the execution relationship is T1 execution ① → ② → ③, which is called serial asynchrony. Parallel means that the main thread creates three tasks ①, ② and ③, and the three tasks are executed by three different threads: T1, T2 and T3. The three threads execute three different tasks at the same time, and the execution is T1 → ①, T2 → ② and T3 → ③. This situation is called parallel asynchronous.

ok ~, the previous example: assuming that it is necessary to sum all odd and even numbers in 100 respectively, the implementation is as follows:

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        CompletableFuture cf1 =
            CompletableFuture.supplyAsync(CompletableFutureDemo::evenNumbersSum);
        CompletableFuture cf2 =
            CompletableFuture.supplyAsync(CompletableFutureDemo::oddNumbersSum);

        // Prevent the main thread from dying, causing the thread executing asynchronous tasks to terminate
        Thread.sleep(3000);
    }

    // Sum even numbers within 100
    private static int evenNumbersSum() {
        int sum = 0;
        System.out.println(Thread.currentThread().getName()
                    +"thread ...Summation is performed even....");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) sum += i;
        }
        return sum;
    }

    // Sum odd numbers within 100
    private static int oddNumbersSum() {
        int sum = 0;
        System.out.println(Thread.currentThread().getName()
                    +"thread ...Odd sum performed....");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) sum += i;
        }
        return sum;
    }
    /* *
     * Execution results:
     *      ForkJoinPool.commonPool-worker-1 Thread... Performed summation even
     *      ForkJoinPool.commonPool-worker-2 Thread... Performed sum odd
     * */
}

In the above case, two tasks cf1 and cf2 are created to perform the logic of summing even boxes and odd numbers respectively. At the same time, in order to observe the effect of parallel execution, sleep for 1 second in two calculation methods: evenNumbersSum and oddNumbersSum, respectively, to simulate the task execution time. It can also be clearly observed from the results of the above case that the two tasks are executed by two different threads worker-1 and worker-2 in parallel.

4.3.3 timing relationship between CompletionStage tasks - aggregation

There are always two types of methods in the CompletionStage interface to describe the aggregation relationship between tasks. One type is and, which represents the methods that start processing after the tasks are executed. The other is or type, which represents the method that the task starts processing as long as any execution is completed. The methods provided are as follows:

  • AND type:
    • thenCombine series: it can receive the results of previous tasks for aggregation calculation, and return values after calculation
    • thenAcceptBoth series: you can receive the results of previous tasks for aggregation calculation, but there is no return value after calculation
    • runAfterBoth series: the results of previous tasks cannot be received and returned, but aggregation calculation can be performed after the task is completed
    • allOf series of completabilefuture class: it cannot receive the results of previous tasks, but it can aggregate multiple tasks, but it should be used together with callback processing methods
  • OR type:
    • applyToEither series: receive the first completed task result for processing, and return the value after processing
    • acceptEither series: receive the first completed task result for processing, but cannot return after processing
    • runAfterEither series: the return value of the previous task cannot be received and there is no return. The order can carry out subsequent processing for the first completed task
    • anyOf series of completabilefuture class: it can aggregate any task at the same time and receive the results of the first completed task for processing. There is no return value after processing. It needs to be used together with the callback method

ok ~, as mentioned above, the CompletionStage interface provides three series of methods for describing the aggregation relationship of AND type OR type between tasks, AND the functions of these three series of methods are basically the same. The difference lies in the different input parameter types. In fact, it is only the difference between the three functional interfaces of BiFunction, BiConsumer AND Runnable. In addition to the three series provided by the CompletionStage interface, the completabilefuture class also extends two methods: anyOf AND allOf. The purpose of these two methods is to aggregate multiple tasks at the same time. The previous one can only aggregate two tasks at the same time for processing. Next, let's look at the case:

public class CompletableFutureDemo {
    public static void main(String[] args) throws Exception {
        /*--------------------Create two asynchronous tasks CF1/CF2------------------*/
        CompletableFuture<Integer> cf1 =
                CompletableFuture.supplyAsync(CompletableFutureDemo::evenNumbersSum);
        CompletableFuture<Integer> cf2 =
                CompletableFuture.supplyAsync(CompletableFutureDemo::oddNumbersSum);

        /*--------------------Test AND type aggregation method------------------*/
        CompletableFuture<Integer> cfThenCombine = cf1.thenCombine(cf2, (r1, r2) -> {
            System.out.println("cf1 Task calculation results:" + r1);
            System.out.println("cf2 Task calculation results:" + r2);
            return r1 + r2;
        });
        System.out.println("cf1,cf2 task ThenCombine Aggregation processing results:" + cfThenCombine.get());

        // Thenaccept both and runAfterBoth series are almost the same as thenCombine
        // The difference lies in the difference between the three functional interfaces of input parameter BiFunction, BiConsumer and Runnable

        // Use allOf to aggregate two tasks (multiple tasks can be aggregated)
        CompletableFuture cfAllOf = CompletableFuture.allOf(cf1, cf2);
        // Used with thenAccept successful callback function
        cfAllOf.thenAccept( o -> System.out.println("Conduct post-processing after all tasks are completed...."));

        //Split line
        Thread.sleep(2000);
        System.err.println("--------------------------------------");

        /*--------------------Test OR type aggregation method------------------*/
        CompletableFuture<Integer> cfApplyToEither = cf1.applyToEither(cf2, r -> {
            System.out.println("First completed task results:" + r);
            return r * 10;
        });
        System.out.println("cf1,cf2 task applyToEither Aggregation processing results:"+cfApplyToEither.get());


        // The acceptEither, runAfterEither series are almost the same as the applyToEither series
        // The difference lies in the difference between the three functional interfaces of input parameter BiFunction, BiConsumer and Runnable

        // Use anyOf to aggregate two tasks, and the execution result of the one who completes the task first will be processed
        CompletableFuture cfAnyOf = CompletableFuture.anyOf(cf1, cf2);
        // Used with thenAccept successful callback function
        cfAnyOf.thenAccept(r -> {
            System.out.println("First completed task results:" + r);
            System.out.println("Follow up the task results completed first....");
        });
    }

    // Sum even numbers within 100
    private static int evenNumbersSum() {
        int sum = 0;
        try {
            Thread.sleep(800); // Simulation time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 == 0) sum += i;
        }
        return sum;
    }

    // Sum odd numbers within 100
    private static int oddNumbersSum() {
        int sum = 0;
        try {
            Thread.sleep(1000); // Simulation time
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        for (int i = 1; i <= 100; i++) {
            if (i % 2 != 0) sum += i;
        }
        return sum;
    }
}

Briefly analyze the above case. First, two asynchronous tasks are created: ① sum all even numbers in 100 and ② sum all odd numbers in 100.
And type test: when both tasks are completed, the odd and even sum up all the numbers in 100.
or type test: after any task is completed, the execution result of the task is x10 times.
In order to observe the results, we sleep for 0.8s in the even summation method and 1s in the odd summation method, so that there is a gap between the execution time of the two tasks, which can clearly let us observe the test results of or type.
ok~, the results of the above case are as follows:

/* *
 * Execution results:
 *       cf1 Task calculation result: 2550
 *       cf2 Task calculation result: 2500
 *       cf1,cf2 Task ThenCombine aggregation processing result: 5050
 *       After all tasks are completed, conduct post-processing
 *       --------------------------------------
 *       First completed task result: 2550
 *       cf1,cf2 Task applyToEither aggregation processing result: 25500
 *       First completed task result: 2550
 *       Follow up the task results completed first
 * */

5, Analysis of asynchronous callback principle of completable future

The principle of completable future is more complicated if you want to study it deeply, because it needs to be understood in combination with thread pool. At the same time, this chapter is long enough, so our in-depth study on the principle will be put in the subsequent articles, and then peek from the perspective of source code. Here is a brief talk about the principle of completable future:

  • ① Why can completable future implement asynchronous callback and asynchronous notification of results?
    Because the created asynchronous task will be executed by a new thread when it is executed. After the task is executed, the logic of the callback function is also processed by the thread executing the task.
  • Why can completable future realize chain programming and complete the serial creation and execution of tasks?
    Because completabilefuture implements the CompletionStage interface, a completabilefuture object is returned after each task is completed. When using, we can continue to create new tasks based on this object. At the same time, there is a linked list in each completabilefuture object, and a newly created task arrives. If the thread has not completed the current task, The new task will be added to the linked list for waiting, and the thread will then execute the tasks in the linked list after processing the current task.

Topics: Java Multithreading