How to implement JCU futuretask

Posted by xtops on Fri, 28 Jan 2022 04:46:47 +0100

I What is Future?

1. What is the future?

The future of JDK is similar to the order number of things purchased on our website. When we execute a time-consuming task, we can start another thread to execute the time-consuming task asynchronously, and we can do other things at the same time. When things are done, we can extract the execution results of time-consuming tasks according to the "order number" of future. Therefore, future is also an application mode in multithreading.

extend: Speaking of multithreading, so Future Also with Thread What's the difference? The most important difference is Thread There is no result returned, and Future Patterns have returned results.

2. How to use Future

We have figured out what Future is. Let's take a simple example to see how to use Future.

If we want to play hot pot now, we must first prepare two things: Boil the water and prepare the ingredients. Because boiling water is a relatively long process (equivalent to time-consuming business logic), we can prepare hot pot ingredients (main thread) while boiling water (equivalent to starting another thread). When both are ready, we can start playing hot pot.

public class DaHuoGuo {
    public static void main(String[] args) throws Exception {
        FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                println(Thread.currentThread().getName() + ":" + "Start boiling water...");
                // Time consuming for simulating boiling water
                Thread.sleep(2000);
                println(Thread.currentThread().getName() + ":"  + "The boiling water has been boiled...");
                return "boiling water";
            }
        });

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

        // do other thing
        println(Thread.currentThread().getName() + ":"  + " At this point, a thread execution is started future Logic (boiling water), at this time, we can do something else (such as preparing hot pot ingredients)...");
        // Simulating the time-consuming of preparing hot pot ingredients
        Thread.sleep(3000);
        println(Thread.currentThread().getName() + ":"  + "Hot pot ingredients are ready");
        String shicai = "Hot pot ingredients";

        // The boiling water has been slightly better. We have obtained the boiled water
        String boilWater = futureTask.get();

        println(Thread.currentThread().getName() + ":"  + boilWater + "and" + shicai + "It's ready. We can start playing hot pot");
    }

    public static void println(String content){
        SimpleDateFormat sdf = new SimpleDateFormat();// Format time 
        sdf.applyPattern("HH:mm:ss");// a is the mark of am/pm
        Date date = new Date();// Get current time 
        System.out.println("["+sdf.format(date)+"] "+content);
    }
}

// [14:46:51] main: at this time, a thread is started to execute the logic of future (boiling water). At this time, we can do other things (such as preparing hot pot ingredients)
// [14:46:51] Thread-0: start boiling water
// [14:46:53] Thread-0: the boiling water has been cooked
// [14:46:54] main: hotpot ingredients are ready
// [14:46:54] main: boiling water and hot pot ingredients are ready. We can start playing hot pot

From the above code, we can see that we use Future mainly in the following steps:

  1. Create a Callable anonymous function implementation class object, and our business logic is implemented in the call method of Callable, where the generic type of Callable is the return result type;
  2. Then the Callable anonymous function object is passed in as the construction parameter of futuretask to build a futuretask object;
  3. Then, the futureTask object is passed in as a Thread construction parameter and the Thread is started to execute the business logic;
  4. Finally, we call the get method of futureTask object to get the execution result of business logic.

You can see that the JDK classes related to the use of Future mainly include FutureTask and Callable. The following is the source code analysis of FutureTask.

Extension: there is another use Future The way is to Callable The implementation class is submitted to the thread pool for execution. It will not be introduced here, but Baidu itself.

II FutureTask source code analysis

(1) Member variables and member methods of FutureTask

  1. Let's first look at the class structure of FutureTask:

You can see that FutureTask implements the RunnableFuture interface, and the RunnableFuture interface inherits the Future and Runnable interfaces. Because FutureTask indirectly implements the Runnable interface, it can be executed by Thread as a task; In addition, the most important point is that FutureTask also indirectly implements the Future interface, so it can also obtain the results of task execution.

  1. Member variable
    Let's first look at the member variables of FutureTask. Understanding these member variables is very important for the later source code analysis.
    /** Encapsulated Callable object whose call method is used to execute asynchronous tasks */
    private Callable<V> callable;
    /** Used to load the execution results of asynchronous tasks */
    private Object outcome;
    /** The thread that executes the callable task */
    private volatile Thread runner;
    /** Thread waiting node, an implementation of reiber stack */
    private volatile WaitNode waiters;
    /** Task execution status */
    private volatile int state;
    
    private static final sun.misc.Unsafe UNSAFE;
    // The field offset used when cas is used to modify member variables with Unsafe
    private static final long stateOffset;
    private static final long runnerOffset;
    private static final long waitersOffset;
    
    // Static block
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = FutureTask.class;
            stateOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("state"));
            runnerOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("runner"));
            waitersOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("waiters"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    

(2) State change of FutureTask

Previously, we talked about the member variable of FutureTask. There is a member variable state that represents the state. We should focus on the state variable, which represents the state of task execution.

private volatile int state;
/** Task creation status */
private static final int NEW          = 0;
/** The task is being completed, which is a transient transition state */
private static final int COMPLETING   = 1;
/** Normal end status of task */
private static final int NORMAL       = 2;
/** Abnormal status of task execution */
private static final int EXCEPTIONAL  = 3;
/** The task is cancelled, corresponding to cancel(false) */
private static final int CANCELLED    = 4;
/** Task interruption state is a transient transition state */
private static final int INTERRUPTING = 5;
/** The task is interrupted, corresponding to cancel(true) */
private static final int INTERRUPTED  = 6;

You can see that the task state variable state has the above seven states, and 0-6 correspond to each state respectively. At first, the task status is NEW, and then the three methods set,setException and cancel of FutureTask set the status changes. There are four kinds of status changes:

  • NEW -> COMPLETING -> NORMAL:
    This state change indicates the normal end of the asynchronous task, in which COMPLETING is an instantaneous temporary transition state, and the state change is set by the set method;
  • NEW -> COMPLETING -> EXCEPTIONAL:
    This state change indicates that an exception is thrown during the execution of asynchronous tasks, and the state change is set by setException method;
  • NEW -> CANCELLED:
    This state change indicates that it is cancelled, that is, cancel(false) is called, and the state change is set by the cancel method;
  • NEW -> INTERRUPTING -> INTERRUPTED:
    This state change indicates that it is interrupted, that is, cancel(true) is called, and the state change is set by the cancel method.

(3) run() method

public void run() {
    // In order to ensure that only one thread is executing futureTask, it is necessary to ensure that the two submissions are satisfied at the same time, otherwise it will be returned directly from the run() method
    //  (1) The status of futuretask is new
    //  (2) The execution thread of futureTask at this time is null, that is, no thread has executed the futureTask
    // What kind of calling method will make multiple threads execute a futureTask for dysmenorrhea? 
    //  Answer: instantiate a futureTask object, and then call new Thread (futureTask) many times. start()
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        // After the code is executed, it has been ensured that only one thread can execute futureTask, 
        // So call callable. directly in the current thread. Call(); Calling: 
        //  (1) If an exception occurs: the update status is EXCEPTIONAL, use the method setException()?
        //  (2) If no exception occurs, the update status is NORMAL, and the method set() is used
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                set(result);
        }
    } finally {
        // After the code is executed here, it has been ensured that only one thread can execute futureTask
        // No matter whether the current thread throws an exception or not, the runner property of futureTask should be set to null after execution
        // Indicates that the current thread has completed execution
        runner = null;
        // The last three lines are the case of being interrupted during processing and execution, because the run() method cannot respond to interrupts in real time,
        // Only detect interrupts through code logic (refer to while(!Thread.currentThread.isInterrupted()) loop),
        // Therefore, in the case of responding to an interrupt after code execution, s > = interrupting, the processing method is as follows: 
        //  private void handlePossibleCancellationInterrupt(int s) {
        //      if (s == INTERRUPTING)
        //          while (state == INTERRUPTING)
        //              Thread.yield();
        //  }
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield();
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

It is worth noting here that when the thread is full and does not meet the conditions for executing asynchronous tasks, whether the runner is null is determined and set by calling the CAS method compareAndSwapObject of UNSAFE. At the same time, compareAndSwapObject assigns a value to the runner through the offset address runnerOffset of the member variable runner. In addition, The member variable runner is modified as volatile. In the case of multithreading, the setting value of the volatile modification variable of one thread can be immediately brushed into main memory, so the value can be seen by other threads.

(4) State change methods of FutureTask: set() and setException()

protected void set(V v) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // Save the final execution result of run() to the outcome member
        outcome = v;
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state NORMAL
        finishCompletion();
    }
}

protected void setException(Throwable t) {
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // Save the final execution result of run() to the outcome member
        outcome = t;
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state EXCEPTIONAL
        finishCompletion();
    }
}

(5) Wake up waiting thread method of FutureTask

Because both set (V, V) and setException(Throwable t) methods finally call finishcompletement (), which means that the asynchronous task will perform some unified operations no matter whether it ends normally or abnormally. These operations are mainly to wake up all threads that are blocked because the asynchronous task has not been completed when calling get() method These blocking threads are wrapped into WaitNode classes to form stack storage Therefore, the order of wake-up (removal) is "last in, first out", that is, the next first in thread is awakened (removed) first. We will continue to analyze how this thread waits for the linked list to form a chain later.

private void finishCompletion() {
    // waiters is a member variable of FutureTask. Every thread blocked by calling get() will be blocked
    // Packaged as WaitNode object (see the definition below), all blocked threads will form a linked list storage The first thing you see is the outer layer
    // In fact, the for loop is a guarantee of "completely clearing all waitnodes". What really traverses the linked list to wake up is
    // Internal for (;) Circulation; This guarantee is needed because the starting point of the action is: 
    //     if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) 
    // Judgment. This judgment can only ensure that there is no new thread at that time. Because get() is added to the waiting queue, it needs to add the outer for detection
    for (WaitNode q; (q = waiters) != null;) {
        // Judge that no new thread is added to the waiting queue of get()
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            // All the following codes are common to traverse the linked list and perform the operation of waking up the internal thread of WaitNode
            for (;;) {
                Thread t = q.thread;
                if (t != null) {
                    q.thread = null;
                    LockSupport.unpark(t);
                }
                WaitNode next = q.next;
                if (next == null)
                    break;
                q.next = null; // unlink to help gc
                q = next;
            }
            break;
        }
    }
    // Meaningless, the method body of done() is empty in version 1.8 {}
    done();  
    // Because the asynchronous task has been executed and the results have been saved to the outcome, you can set the callable object to be empty at this time
    callable = null;
}

[note]: WaitNode definition:

static final class WaitNode {
    volatile Thread thread;   // Wrapper thread
    // The flag of the linked list (actually the stack. Use the top element of the stack to perform cas judgment to determine whether a new thread joins the get() waiting queue)
    volatile WaitNode next;   
    WaitNode() { thread = Thread.currentThread(); }
}

(6) Futuretask Get method to obtain the task execution result

Previously, after we start a thread to execute asynchronous tasks in its run method, we can call futuretask Get method to get the result of asynchronous task execution.

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    // (1) If the task state is < = completing, it indicates that the asynchronous task is in the process of execution,
    //     The awaitDone method is called to block the wait
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // (2) When the code is executed here, it indicates that the waiting thread has been awakened and the task has been executed: 
    //     The task may be executed successfully or failed. report() will be executed according to the execution status
    //     Choose normal return or throw exception The definition is detailed below
    return report(s);
}
  1. awaitDone() method
// The reason for the two parameters is that some threads only want to wait for a limited time when calling get()
// Wait until the end of the task to return the normal get(), and the timed parameter is false   
private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    // Calculate the maximum waiting time point Take 0 as the time point when the waiting time is not limited
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    WaitNode q = null;
    // Not stacked yet
    boolean queued = false;
    for (;;) {
        // (1) Wait for the thread to be interrupted and throw an exception to exit
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }
        // Task execution status
        int s = state;
        // (2) S > completing indicates that the task has been completed, return to the final state and exit 
        //     The task may end normally, and an exception may be thrown,
        //     Or the task is CANCELLED (one of the status of canceled, INTERRUPTING or INTERRUPTED)
        if (s > COMPLETING) {
            // [question] at the end of the task, the run() method will also call finishcompletement (), which will wait for the data in the stack
            //      The thread of WaitNode node is set to null. Why call q.thread = null to clear again?
            // [answer] because if there are many threads to obtain the task execution results, the thread of the task will be obtained at the moment when the task is completed
            //      Or it is already in the thread waiting list; Or it is still an isolated WaitNode node at this time.
            //      (1) All WaitNode nodes in the thread waiting list will be removed (awakened) by finishcompletement
            //         Wait for the WaitNode node for garbage collection;
            //      (2) The isolated thread WaitNode node is not blocked at this time, so it does not need to be awakened. At this time, just set its attribute to
            //         null, and then whether it is referenced by anyone, so it can be GC.
            if (q != null)
                q.thread = null;
            return s;
        }
        // The task is still in progress. Continue to wait
        else if (s == COMPLETING) 
            Thread.yield();
        // If the node has not been constructed, construct the node
        else if (q == null)
            q = new WaitNode();
        // Add the constructed node to the head of the thread waiting stack
        // [question]: why should the action of adding nodes to the stack be written in the loop? 
        // [answer]: This is the standard way to stack cas nodes under multithreading Because the stack action may fail, it is written in
        //       Continuously stack in the dead cycle; This is also the key to judge the else if (q == null) branch within the loop
        //       Reason: this branch ensures that the node is constructed only once, but the stack action can be executed countless times until it is successful
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        // Processing the wait of get() thread: 
        else if (timed) {
            nanos = deadline - System.nanoTime();
            // The wait has timed out
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            // Wait did not time out, continue to wait for the expected time
            LockSupport.parkNanos(this, nanos);
        }
        // Handling unlimited get(): 
        else
            // Thread enters blocking waiting state
            LockSupport.park(this);
    }
}

Generally speaking, it is a way to write the code logic that can be written together, such as putting the node into the stack after constructing the node, then blocking the three successive actions of the thread in the node, flattening it into branches of the same level and writing it in the dead loop, which takes into account the failure of cas operation This ensures that the continuous actions without cas can be executed like in the next for loop

  1. report() method
private V report(int s) throws ExecutionException {
    // results of enforcement
    Object x = outcome;
    // (1) Normal return
    if (s == NORMAL)
        return (V)x;
    // (2) Abnormal exit due to task cancellation
    if (s >= CANCELLED)
        throw new CancellationException();
    // (3) Task failed exit
    throw new ExecutionException((Throwable)x);
}

(7) Futuretask Cancel method to cancel the execution of the task

As you can see below, cancellation can only be executed when the cancel action is executed and there is no thread executing the task

public boolean cancel(boolean mayInterruptIfRunning) {
    // Status= NEW, the task is already in progress and cannot be cancelled
    // When cas modifies the status, it is found that the status is not NEW, indicating that a NEW thread has executed the task, and the task cannot be cancelled
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    
        // If runxu interrupts, interrupt the thread
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    t.interrupt();
            } finally { // final state
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    } finally {
        // Finally wake up the waiting thread in the blocking stack
        finishCompletion();
    }
    return true;
}

III summary

In general, the simplest way to implement the future mode is as follows:

  • (1) declare a volatile tag variable to mark whether the task has been completed
  • (2) when the execution is not completed, the thread calling get() executes flag Just wait () The conditional waiting queue inside the jvm is utilized
  • (3) execute the run() method with a thread

In contrast, the implementation of javaSE has several extensions:

  • (1) the marked variable is not only true/false, but also a series of states: new, completing, NORMAL, EXCEPTIONAL, etc This is mainly to match the logic of run(),get(),cancel() under multithreading
  • (2) on the problem of get () thread blocking, javaSE does not use the synchronized conditional wait queue, but uses cas to operate the wait stack When the new thread executes get() blocking, other threads sense that the new thread is obtained by checking whether the top node of the stack has changed through cas
  • (3) for the run() method, through the implementation of javaSE, set the member variable volatile Thread runner to limit that at most one thread can execute the run() method at the same time
  • (4) in addition, the javaSE version also implements methods such as cancel