Source Code Analysis-FutureTask

Keywords: Java

Basic principles

Firstly, FutureTask was implemented by AQS in its early stage. The current version (java 7) saves all States with a volatile int. And use unsafe's cas to modify it. Of course, if you use an Atomic class, you can. But unsafe is more efficient.

State introduction

    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;

First of all, the basic definition of state is as follows, which can be seen from the name.
First, the initial state is NEW.
Only after the set, setException, and cancal methods are invoked will they become terminal.
During completion, the state becomes COMPLETING (setting results) or INTERRUPTING (interrupting tasks to try to satisfy cancel(true).
The process from these intermediate states to the final state is written in order/laziness because these values are unique and will not be modified.

Possible state transitions

NEW -> COMPLETING -> NORMAL
NEW -> COMPLETING -> EXCEPTIONAL
NEW -> CANCELLED
NEW -> INTERRUPTING -> INTERRUPTED

Internal domain

    private volatile int state;
    private Callable<V> callable;//The underlying callable will be cleaned up after running
    private Object outcome; // Computed results or exceptions that need to be thrown do not use volatile, but use state read and write
    private volatile Thread runner;//A thread running callable. Using cas settings in run() method
    private volatile WaitNode waiters;//Collection of waiting nodes

The meaning is relatively clear.

WaitNode Internal Static Class

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

WaitNode is a simple one-way list. I want to talk about it here. Questions about internal classes. I've been struggling with the situation in which internal classes should be used and the situation in which internal static classes should be used. But some time ago I saw a paragraph in effectivejava that said better. Simply put. Internal static classes should be preferred because they are safer and do not bind instances of external classes. That is to say, static classes should be used as long as there is no need for internal classes to access the contents of external class instances. Specific can be the content of the book.

Main method

constructor

First, the constructor has nothing to say, just set the following callable and state. Here's a way to wrap runnable and result into callable.

this.callable = Executors.callable(runnable, result);

get() method

    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)//If the current state is below COMPLETING, run awaitDone to wait for the calculation to complete or
            s = awaitDone(false, 0L);
        return report(s);
    }

Next, the two methods awaitDone and report still need to look at:

awaitDone

    private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        final long deadline = timed ? System.nanoTime() + nanos : 0L;//Calculate the possible delay based on the parameters, reserve the WaitNode reference and the entry flag bit.
        WaitNode q = null;
        boolean queued = false;
        for (;;) {
            if (Thread.interrupted()) {//If the thread executing the task interrupts, you need to remove the Waiter and throw an interrupt exception.
                removeWaiter(q);
                throw new InterruptedException();
            }

            int s = state;
            if (s > COMPLETING) {//If the state is larger than COMPLETING, the task will return to the state when it is completed or interrupted. It is also important to note that the possible thread s of WaitNode are null to prevent possible unexpected operations.
                if (q != null)
                    q.thread = null;
                return s;
            }
            else if (s == COMPLETING) // cannot time out yet // Wait while running. Thread.yield() to avoid excessive spin.
                Thread.yield();
            else if (q == null)//The new WaitNode() is set up instead of before the entire method starts to prevent the calculation from ending early. This allows direct return without construction. Running here shows that it has been delivered before, but it is still incomplete, so it needs to be ready for inclusion.
                q = new WaitNode();
            else if (!queued)//Use cas to set waiters Offset
                queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                     q.next = waiters, q);
            else if (timed) {//Blocking with time limit is performed according to conditions.
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L) {
                    removeWaiter(q);
                    return state;
                }
                LockSupport.parkNanos(this, nanos);
            }
            else
                LockSupport.park(this);
        }
    }

removeWaitNode
Here's a removeWaitNode method
The java version of the goto statement is also used here. Semantic tags. Cooperate with recycling.
Note here that this removeWaitNode is used to delete timeouts or to interrupt your nodes. Convenient operation is used here, if there are many nodes, it will be here.

    private void removeWaiter(WaitNode node) {
        if (node != null) {
            node.thread = null;
            retry:
            for (;;) {//The first for loop doesn't really make sense, it's just for retry: tags. The reason is that goto comes here to retry when a retry occurs.
                for (WaitNode pred = null, q = waiters, s; q != null; q = s) {//The second cycle is the body.
                    s = q.next;
                    if (q.thread != null)
                        pred = q;
                    else if (pred != null) {
                        pred.next = s;
                        if (pred.thread == null) // check for race
                            continue retry;
                    }
                    else if (!UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                          q, s))
                        continue retry;
                }
                break;
            }
        }
    }

What exactly did the second cycle do? Here's a brief analysis.
The pred record is actually a valid node whose last thread is not null. q is a pointer to the current node. The loop traverses the node from beginning to end until the end of the node. s is the successor of q, and the node may be null.

From the point of view of the current node, there are only two cases. One is that the thread of the current node q is null, that is to say, the current node is invalid. If pred node exists, the next of pred is s, that is to say, the current node is skipped. Enter the next loop (where in fact a competitive judgement is made to ensure that pred threads are not null. If the conditions are not met, the heavy-headed nodes need to start again, which is of course very time-consuming. So it's actually assumed that there won't be too many nodes and the competition won't be too fierce. This is introduced in the source code.) Of course, if pred is null, it means that there are no valid nodes before the current node, then we need to set waiters Offset as the current s. If you fail, start again.

If the thread of the current node q is not null, try to set pred to the current node and enter the next loop.

The removeWaiter's job is actually to throw away all thread ed null nodes.

report
There is also a report method. This is simply to return results or throw exceptions based on the current state.

    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

In fact, there is a question that I think can be discussed slightly. That's why you put the thrown exception in outcom. I think it is quite appropriate to define a new domain.

run()

    public void run() {
        if (state != NEW ||//This judgment is intended to prevent concurrent errors. The status is not new, indicating that it has been started. The current thread setup failure indicates that a thread has started to perform this task.
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;//ran serves as a sign of completion of the run, depending on whether to call set or setException.
                try {
                    result = c.call();//Call c.call() in the current thread;
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;//runner must be set up to ensure that the call run is not concurrent.
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)//Reread state after setting runner to null to prevent missing interrupts.
                handlePossibleCancellationInterrupt(s);
        }
    }

The run method is relatively simple. Here's another look at the set, setException, and handlePossibleCancellation Interrupt methods:

set

    protected void set(V v) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = v;
            UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
            finishCompletion();
        }
    }

After CAS sets state successfully, set output, and then set NORMAL as normal termination state in CAS.

finishCompletion
This method is mainly used to wake up all waiting threads.
This method is much simpler than removeWaitNode, which is to unlock all valid nodes.

    private void finishCompletion() {
        // assert state > COMPLETING;
        for (WaitNode q; (q = waiters) != null;) {
            if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
                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;
            }
        }

        done();//A hook method.

        callable = null;        // to reduce footprint
    }

handlePossibleCancellationInterrupt

This method is more interesting. This method is called only when cancel(true) is invoked. This method is used in conjunction with cancel.

    private void handlePossibleCancellationInterrupt(int s) {
        if (s == INTERRUPTING)//If the status is INTERRUPTING, cancel is called and canceled successfully. At this point, another thread will attempt to terminate the current thread. But we know that interrupts are not immediately valid in java, so we call Thread.yield() to try to compromise the current thread. The INTERRUPTED program will not continue running until the task of the current thread is cancelled.
            while (state == INTERRUPTING)
                Thread.yield(); // wait out pending interrupt
    }

setException
Like set, it simply sets the state to Exception.

cancel

    public boolean cancel(boolean mayInterruptIfRunning) {
        if (state != NEW)//Once the task starts, there is no way to cancel it.
            return false;
        if (mayInterruptIfRunning) {
            if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, INTERRUPTING))//Attempt to set the current state to try to interrupt.
                return false;
            Thread t = runner;
            if (t != null)//If a thread has been set, try to interrupt it.
                t.interrupt();
            UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); //This state is final.
        }
        else if (!UNSAFE.compareAndSwapInt(this, stateOffset, NEW, CANCELLED))//If the input parameter is false, it means that the interrupt state is not needed, but the cancel state is attempted directly.
            return false;
        finishCompletion();
        return true;
    }

Posted by Vikas Jayna on Sun, 07 Apr 2019 05:51:31 -0700