Notes after reading "The Art of Java Concurrent Programming" - Detailed Explanation of FutureTask (Chapter 10)

Notes after reading "The Art of Java Concurrent Programming" - Detailed Explanation of FutureTask (Chapter 10)

1. Introduction to FutureTask

In addition to implementing the interface, FutureTask Futurealso implements Runnablethe interface. Therefore, FutureTask can be handed over to Executor for execution, or it can be executed directly by the calling thread ( FutureTask.run()).

According to the timing when the FutureTask.run() method is executed, the FutureTask can be in the following three states:

  1. Not started . Before the FutureTask.run() method is executed, the FutureTask is not started. When a FutureTask is created and the FutureTask.run() method is not executed, the FutureTask is not started.
  2. started . During the execution of the FutureTask.run() method, the FutureTask is in the started state.
  3. completed . The FutureTask.run() method ends normally after execution, or is canceled (FutureTask.cancel(…)), or an exception is thrown when the FutureTask.run() method is executed and ends abnormally, and the FutureTask is in a completed state.

FutureTaskA schematic diagram of the state transition:

image-20220118160357680

For the FutureTask.get() method :

  • When the FutureTask is not started or started, executing the FutureTask.get() method will cause the calling thread to block
  • When the FutureTask is in the completed state, executing the FutureTask.get() method will cause the calling thread to immediately return the result or throw an exception.

For the FutureTask.cancel() method :

  • When the FutureTask is not started, executing the FutureTask.cancel() method will cause the task to never be executed
  • When the FutureTask is in the started state, executing the FutureTask.cancel(true) method will try to stop the task by interrupting the thread executing the task
  • When the FutureTask is in the started state, executing the FutureTask.cancel(false) method will not affect the thread that is executing the task (let the executing task run to completion)
  • When the FutureTask is in the completed state, executing the FutureTask.cancel(…) method will return false.

getSchematic diagram of the method and cancelexecution of the method:

image-20220118161125457

2. The use of FutureTask

You can hand over the FutureTask to the Executor for execution; you can also return a FutureTask through the ExecutorService.submit(…) method, and then execute the FutureTask.get() method or the FutureTask.cancel(…) method. In addition to this, you can also use FutureTask alone

When a thread needs to wait for another thread to complete a task before it can continue to execute, you can use FutureTask

for example: Assuming that there are multiple threads executing several tasks, each task can only be executed once at most. When multiple threads try to execute the same task at the same time, only one thread is allowed to execute the task, and other threads need to wait for the task to be executed before continuing.

code show as below:

/**
 * @author xppll
 * @date 2022/1/18 16:21
 */
public class FutureTaskTest {
    
    
    private final ConcurrentMap<Object, Future<String>> taskCache
            = new ConcurrentHashMap<Object, Future<String>>();

    private String executionTask(final String taskName) {
    
    
        while (true) {
    
    
            Future<String> future = taskCache.get(taskName);
            if (future == null) {
    
    
                Callable<String> task = new Callable<String>() {
    
    
                    @Override
                    public String call() throws Exception {
    
    
                        return taskName;
                    }
                };
                FutureTask<String> futureTask = new FutureTask<String>(task);
                future = taskCache.putIfAbsent(taskName, futureTask);
                if (future == null) {
    
    
                    future = futureTask;
                    futureTask.run();
                }
                try {
    
    
                    return future.get();
                } catch (InterruptedException | ExecutionException e) {
    
    
                    taskCache.remove(taskName, future);
                    e.printStackTrace();
                }

            }
        }
    }

    public static void main(String[] args) {
    
    

    }
}

The schematic diagram of the execution of the above code is as follows:

image-20220118162553379

When two threads try to execute the same task at the same time, if Thread 1 executes 1.3 and Thread 2 executes 2.1, then Thread 2 will wait at 2.2 until Thread 1 executes 1.4 before Thread 2 can start from 2.2 (FutureTask.get( ))return

3. Implementation of FutureTask

FutureTaskThe implementation is based on AbstractQueuedSynchronizer(AQS). AQS is a synchronization framework that provides general mechanisms to atomically manage synchronization state, block and wake up threads, and maintain queues of blocked threads.

For detailed AQS, please refer to: Notes after reading "The Art of Java Concurrent Programming" - Chapter 5 Locks in Java

Every synchronizer implemented based on AQS will contain two types of operations:

  1. at least one acquireaction.
    • This operation blocks the calling thread unless/until the state of the AQS allows the thread to continue executing.
    • The acquire operation of FutureTask is a get()/get(long timeout, TimeUnit unit) method call.
  2. at least one releaseaction.
    • This operation changes the state of the AQS, which allows one or more blocked threads to be unblocked.
    • The release operation of FutureTask includes run() method and cancel(...) method.

based onComposition over inheritanceAccording to the principle, FutureTask declares an internal private subclass inherited from AQS Sync, and calls to all public methods of FutureTask will be delegated to this internal subclass.

AQS is provided as the basic class of the "template method pattern" to FutureTask's internal subclass Sync. This internal subclass only needs to implement the methods of status checking and status updating , which will control the acquisition and release operations of FutureTask. Specifically, Sync implements tryAcquireShared(int)the methods and tryReleaseShared(int)methods of AQS, and Sync uses these two methods to check and update the synchronization status.

The design diagram of FutureTask is shown in the figure:

image-20220204102958563

As can be seen from the figure: Sync is an internal private class of FutureTask, which inherits from AQS. When creating a FutureTask, an internal private member object Sync is created, and all public methods of the FutureTask are directly delegated to the internal private Sync

Next, let's look at the execution process of several methods.

FutureTask.get()The method will call AQS.acquireSharedInterruptibly(int arg)the method, the specific process is as follows:

  1. Call the AQS.acquireSharedInterruptibly(int arg) method. This method will first call back the tryAcquireShared() method implemented in the subclass Sync to determine whether the acquire operation can succeed.

    The condition that the acquire operation can succeed is: the state is the execution completion state RAN or the canceled state CANCELLED, and the runner is not null.

  2. The get() method returns immediately if successful. If it fails, go to the thread waiting queue to wait for other threads to perform the release operation.

  3. When other threads execute the release operation (such as FutureTask.run() or FutureTask.cancel()) to wake up the current thread, the current thread executes tryAcquireShared() again and returns a positive value of 1, and the current thread will leave the thread waiting queue and wake up its successor Thread (here will produce the effect of cascading wake-up, which will be introduced later)

  4. Finally return the calculated result or throw an exception.

FutureTask.run()The execution process is as follows:

  1. Executes the task specified in the constructor (Callable.call()).
  2. Update the synchronization state in an atomic way (call AQS.compareAndSetState(int expect, int update), set the state to the execution completion state RAN). If the atomic operation is successful, set the value of the variable result representing the calculation result to the return value of Callable.call(), and then call AQS.releaseShared(int arg).
  3. AQS.releaseShared(int arg) will first call back the tryReleaseShared(arg) implemented in the subclass Sync to perform the release operation (set the thread runner running the task to null, and then return true); AQS.releaseShared(int arg), and then Wake up the thread waiting for the first thread in the queue.
  4. Call FutureTask. done().

When the FutureTask.get() method is executed, if the FutureTask is not in the execution completion state RAN or in the canceled state CANCELLED, the current execution thread will wait in the thread waiting queue of AQS (see threads A, B, C and D in the figure below) . When a thread executes the FutureTask.run() method or the FutureTask.cancel(...) method, it will wake up the first thread in the thread waiting queue (thread E wakes up thread A shown in the figure below).

image-20220204105829100

Assuming that the FutureTask is not started or started at the beginning, there are already 3 threads (A, B and C) waiting in the waiting queue. At this point, thread D executing the get() method will cause thread D to also wait in the waiting queue.

When thread E executes the run() method, it wakes up the first thread A in the queue. After thread A is awakened, it first removes itself from the queue, then wakes up its successor thread B, and finally thread A returns from the get() method. Threads B, C, and D repeat the processing flow of thread A. Eventually, all threads waiting in the queue are awakened by the cascade and return from the get() method.
insert image description here

Guess you like

Origin blog.csdn.net/qq_45966440/article/details/122782361