Talk about how the Callable task works? How did it get its execution results?

Posted by sabien on Fri, 06 Sep 2019 16:44:02 +0200

Talk about how the Callable task works? How did it get its execution results?

Submitting a Callable task to the thread pool creates a new thread (the thread that executes the task) to execute the Callable task, but getting the execution result of the task through Future#get is in the caller thread that submits the task. Question 1: How does the caller thread get the result of the thread that executes the task?

In JDK, there are two types of tasks, Runnable and Callable, but specifically to the java.util.concurrent.ThreadPoolExecutor#execute(Runnable) method of thread pool execution task, it only receives Runnable task. Question 2: How does the Callable task execute after being submitted to thread pool?

How does the Callable task work?

import java.util.concurrent.*;

public class FutureTest {
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                //sleep is for debugging convenience
                TimeUnit.SECONDS.sleep(4);
                return 3;
            }
        };
        //Create a ThreadPoolExecutor object
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        
        Future<Integer> future = executorService.submit(callable);

        try {
            Integer i = future.get();
            System.out.println(i);
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

Future<Integer> future = executorService.submit(callable);

//java.util.concurrent.AbstractExecutorService#submit(java.util.concurrent.Callable<T>)    
public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        //FutureTask is actually a Runnable Future, Runnable Future is actually a Runnable Future.
        //The key point is: Runnable run method execution, in fact, is FutureTask run method execution!!!
        RunnableFuture<T> ftask = newTaskFor(task);
        //java.util.concurrent.ThreadPoolExecutor#execute
        execute(ftask);
        return ftask;
    }

RunnableFuture<T> ftask = newTaskFor(task);

//java.util.concurrent.AbstractExecutorService#newTaskFor(java.util.concurrent.Callable<T>)
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

When submit a Callable task, a RunnableFuture interface object is generated, and by default the RunnableFuture object is a FutureTask object. Look at the source annotation for the java.util.concurrent.AbstractExecutorService class: We can also override the newTaskFormethod to generate our own RunableFuture. A specific example can refer to the ES source code org.elastic search.common.util.concurrent.Prioritized EsThreadPoolExecutor#newTaskFor (java.util.concurrent.Callable<T>), which rewrites the newTaskFor method and implements the logic of obtaining the results of task execution when performing priority tasks.

the implementation of submit(Runnable) creates an associated RunnableFuture that is executed and returned. Subclasses may override the newTaskFor methods to return RunnableFuture implementations other than FutureTask

Then look at the run() method of the FutureTask class: java. util. concurrent. FutureTask run, which triggers the execution of the Callable call () method we defined. Understanding how the java.util.concurrent.FutureTask run method is invoked is a good way to understand how the thread pool performs the Callable task. This method mainly does two things:

  • Execute the Callable#call method, which is the processing logic we defined in FutureTest.java: Return an Integer 3
  • Set the execution result of the task: set(result)

Excute (ftask) submission task in java.util.concurrent.AbstractExecutorService#submit(java.lang.Runnable) (Note: FutureTask implements Runnable)

ThreadPoolExecutor is the specific implementation class of AbstractExecutorService, so it will eventually execute the submission task: java.util.concurrent.ThreadPoolExecutor#execute.

//java.util.concurrent.ThreadPoolExecutor#execute, focusing on addWorker() implementation
if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }

java.util.concurrent.ThreadPoolExecutor#addWorker has two lines of code that are critical:

//java.util.concurrent.ThreadPoolExecutor#addWorker
try {
            w = new Worker(firstTask);//Key code 1, firstTask is essentially a FutureTask object
            final Thread t = w.thread;
            if (t != null) {
              //... Eliminate non-critical code
                if (workerAdded) {
                    t.start();//Key Code 2
                    workerStarted = true;
                }
            }
        }

W = new Worker (first Task) creates a new thread! Worker is passed in as this object because Worker implements Runnable and implements the java.lang.Runnable#run method.

        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;//
            this.thread = getThreadFactory().newThread(this);
        }

What does that mean? Executing java. lang. Runnable run will actually execute java. util. concurrent. ThreadPoolExecutor. Worker run. Who calls java. lang. Runnable run?

Clever you must know that when new Thread(Runnable).start() is executed, the jvm automatically calls java. lang. Runnable run #run

So, the key code 2T. start () in java. util. concurrent. ThreadPoolExecutor addWorker above triggers the call to java. util. concurrent. ThreadPoolExecutor. Worker run.

In java.util.concurrent.ThreadPoolExecutor.Worker#run, only runWoker(this) is called.

//java.util.concurrent.ThreadPoolExecutor.Worker#run
/** Delegates main run loop to outer runWorker. */
        public void run() {
            runWorker(this);
        }

Here comes the point! Follow up to see where Run Woker is sacred:

//java.util.concurrent.ThreadPoolExecutor#runWorker
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;//task is actually an object of FutureTask type
        w.firstTask = null;
        try {
            while (task != null || (task = getTask()) != null) {
              //Eliminate some non-critical code...
                try {
                    beforeExecute(wt, task);//
                    try {
                        //Key code! Trigger java. util. concurrent. FutureTask run execution
                        task.run();
                        afterExecute(task, null);
                    } catch (Throwable ex) {
                        //Look at the afterExecute method annotation. Whether or not an exception is thrown during thread execution, afterExecute() executes. Look at the source code and see why, because after Execute is executed in catch exception handling.
                        afterExecute(task, ex);
                        throw ex;
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }

After reading java. util. concurrent. ThreadPoolExecutor runWorker, you can almost understand the functions of beforeExecute and afterExecute methods when thread pools perform tasks (such as doing some statistics on the running time of thread pool tasks in the afterExecute method).

Summarize the following points:

  • When the Callable task is submit, a FutureTask object is generated, encapsulating Callable, executing the Callable call method in the run method of FutureTask, and calling java.util.concurrent.FutureTask set to set the results of the Callable task execution (the results are stored in an Object-type instance variable of FutureTask: Object outcome;).

  • Future < Integer > future = executorService. submit (callable); returns a Future, which is actually a FutureTask object that obtains the execution result of the Callable task through java. util. concurrent. FutureTask get ().

  • The java.util.concurrent.FutureTask#run method is triggered by java.util.concurrent.ThreadPoolExecutor#runWorker; and java.util.concurrent.ThreadPoolExecutor#runWorker is triggered by java.concurrent.ThreadPoolor.Executor#runWorker#run; and java.util.concurrent.ThreadPoolor.Executor.Executor#runWorker#runRunWorker It is t.start() in java. util. concurrent. ThreadPoolExecutor addWorker; this statement triggers the call; and t.start(); triggers the execution of the Runnable run method. This is the principle mentioned earlier: new Thread(Runnable).start() is invoked by jvm to Runnable run. Specific reference:

    One word is polymorphism. A chart shows that:

  • When inheriting ThreadPoolExecutor to implement a custom thread pool, you can override the afterExecute() method to implement some exception handling logic. Whether the task is completed properly or an exception is thrown, afterExecute() will be called. You can see the JDK source code annotations on the ThreadPoolExecutor#runWorker method. It is interesting to study the ES SEARCH thread pool source code, which uses afterExecute to calculate the waiting time and execution time of each task submitted to the thread pool, so as to automatically adjust the length of the thread pool task queue according to Little's law: org. elastic search. common. util. concurrent. Queue Resizing EsThreadPool Executor#afterExecute

Finally, I want to say that the Callable task, to the ThreadPool Executor thread pool execution level, is actually a Runnable task execution. Because, when ExecutorService submit it Callable, Callable is actually encapsulated in FutureTask/Runnable Future, and Runnable Future implements Runnable, so it can be submitted to the thread pool for java.util.concurrent.ThreadPoolExecutor#executable (Runnable command) execution, which answers the second one proposed at the beginning of this article. Questions.

//java.util.concurrent.RunnableFuture
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

A chart shows that:

Callable tasks are set and acquired at the level of FutureTask. Callable is encapsulated in FutureTask, and FutureTask implements Runnable, which is transformed into ThreadPool Executor#execute to perform Runnable tasks.

How does the result of the Callable task get? Why does Future.get block?

private volatile int state of java.util.concurrent.FutureTask; variable:

//java.util.concurrent.FutureTask#run
public void run() {
        if (state != NEW ||
            !RUNNER.compareAndSet(this, null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    //Callable call executed successfully, ran=true
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                //ran=true sets the execution result of the Callable task
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

When the set method sets the execution result of the Callable task, it modifies the value of the state instance variable of FutureTask!

    //java.util.concurrent.FutureTask#set   
    protected void set(V v) {
        if (STATE.compareAndSet(this, NEW, COMPLETING)) {
            outcome = v;
            STATE.setRelease(this, NORMAL); // final state
            finishCompletion();
        }
    }

The java. util. concurrent. FutureTask get () method also checks the state value to determine whether the Callable task can be executed.

    //java.util.concurrent.FutureTask#get()
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            //If the state is not in the NORMAL state, FutureTask#get() will block.
            //This is why java. util. concurrent. Future get () is blocked.
            s = awaitDone(false, 0L);//This calls: Thread.yield(), LockSupport.park(this)
        return report(s);
    }

java.util.concurrent.FutureTask#awaitDone

//java.util.concurrent.FutureTask#awaitDone
private int awaitDone(boolean timed, long nanos)
        throws InterruptedException {
        WaitNode q = null;
        //Eliminate some irrelevant code.
        for (;;) {//The for loop keeps checking the status of the task until it can "end"
            int s = state;
            //The value of state is greater than COMPLETING, indicating that the result of the Callable task is available.
            //java.util.concurrent.FutureTask#set sets the result of the Callable task and modifies the value of state
            if (s > COMPLETING) {
                if (q != null)
                    q.thread = null;
                return s;
            }
            //The running status of the COMPLETING task is: in progress
            else if (s == COMPLETING)
                // We may have already promised (via isDone) that we are done
                // so never return empty-handed or throw InterruptedException
                Thread.yield();//Suspend the thread that gets the execution result (that's why Futur#get blocked)
            else if (Thread.interrupted()) {
                removeWaiter(q);//Tasks may be interrupted, and of course there is no need to wait for execution results.
                throw new InterruptedException();
            }
            else if (q == null) {
                if (timed && nanos <= 0L)
                    return s;
                q = new WaitNode();
            }
            else if (!queued)
                queued = WAITERS.weakCompareAndSet(this, q.next = waiters, q);
            //Implementation Principle of java.util.concurrent.Future#get(long, java.util.concurrent.TimeUnit) Timeout Blocking
            else if (timed) {
                final long parkNanos;
                if (startTime == 0L) { // first time
                    startTime = System.nanoTime();
                    if (startTime == 0L)
                        startTime = 1L;
                    parkNanos = nanos;
                } else {
                    long elapsed = System.nanoTime() - startTime;
                    if (elapsed >= nanos) {
                        removeWaiter(q);
                        return state;
                    }
                    parkNanos = nanos - elapsed;
                }
                // nanoTime may be slow; recheck before parking
                if (state < COMPLETING)
                    LockSupport.parkNanos(this, parkNanos);
            }
            else
                LockSupport.park(this);
        }
    }

Summarize: The state variable is used to determine whether the execution result of the Callable task has been generated. If the execution result has been generated, then java.util.concurrent.FutureTask#set puts the result in the variable private Object outcome; outcom. Then set the value of state to NORMAL, and java. util. concurrent. FutureTask get () checks the value of state to get the execution result. Of course, if the execution result has not been generated, java. util. concurrent. FutureTask awaitDone will cause get blocking.

Finally, let's leave a question: Since Future#get is blocked in JDK, is there any way to get the execution result of the Callable task without blocking?

Look at Netty's source code? Learn from the Listener callback mechanism. Haha...

Topics: Java JDK jvm Netty