Java Concurrent Programming Notes: J.U.C. Excutors Framework: Excutors Framework Design Concept

Keywords: Programming JDK Java

Introduction of executors framework

juc-executors framework is the most complex framework of class/interface relationship in the whole J.U.C package. The premise of truly understanding executors framework is to clarify the relationship between each module, build a high house, and thoroughly understand the functions and design ideas of each module from the whole to the part.

There are too many articles on the Internet about executors framework, either in general, or blind to Mount Taishan, lack of a holistic perspective, many simply do not understand the design ideas and modular relationships of the whole framework. This paper will review the whole executors framework, introduce the functions and links of each module, and then discuss each module in depth, including each tool class in the module.

From Executor

When Executor is JDK 1.5, with the introduction of an interface by J.U.C, the main purpose of introducing this interface is to decouple the task itself and the execution of the task. When we execute a task through a thread, we often need to create a thread first, and then call the start method of the thread to execute the task:

new Thread(new(RunnableTask())).start();
The above Runnable Task is a task class that implements the Runnable interface. The Executor interface decouples the execution of tasks and tasks. There is only one way for the interface to participate in the tasks to be executed:
public interface Executor {
    /**
     * Execute a given Runnable task.
     * According to the implementation of Executor, the specific execution mode is also different.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be accepted for execution
     * @throws NullPointerException       if command is null
     */
    void execute(Runnable command);
}

We can perform tasks as follows without concern for thread creation:

Executor executor = someExecutor;       // Create concrete Executor objects
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
...

Because Executor is only an interface, the specific ways to perform tasks vary according to its implementation, such as:

Synchronized task execution

class DirectExecutor implements Executor {   
    public void execute(Runnable r) {
        r.run();
    }
}

DirectExecutor is a synchronous task executor. For incoming tasks, execute will return only after execution is complete.

(2) Asynchronous Task Execution

/** Fallback if ForkJoinPool.commonPool() cannot support parallelism */
    static final class ThreadPerTaskExecutor implements Executor {
        public void execute(Runnable r) { new Thread(r).start(); }
    }

ThreadPerTaskExecutor is an asynchronous task executor. For each task, the executor creates a new thread to execute the task.

Note: Java threads are mapped to threads in the local operating system. When a Java thread starts, a local operating system thread is created; when the Java thread terminates, the corresponding operating system thread is reclaimed. Because the CPU resources are limited, the number of threads is limited, so the thread pool is usually used to manage the creation / recovery of threads, which is actually the embryonic form of the thread pool.

(3) Queuing tasks

class SerialExecutor implements Executor {
  final Queue<Runnable> tasks = new ArrayDeque<Runnable>();
  final Executor executor;
  Runnable active;

  SerialExecutor(Executor executor) {
    this.executor = executor;
  }

  public synchronized void execute(final Runnable r) {
    tasks.offer(new Runnable() {
      public void run() {
       try {
         r.run();
       } finally {
         scheduleNext();
       }
      }
    });
    if (active == null) {
      scheduleNext();
    }
  }

  protected synchronized void scheduleNext() {
    if ((active = tasks.poll()) != null) {
      executor.execute(active);
    }
  }
}

Serial Executor queues incoming tasks (FIFO sequence) and then takes out a task from the head of the queue to execute.

These examples just give some possible Executor implementations. Many Executor implementation classes are provided in the J.U.C. package. As we will talk about later, the key here is to understand the design idea of Executor - decoupling the execution of tasks and tasks.

Enhanced Executor-Executor Service

The function of Executor interface is very simple. In order to enhance it, J.U.C. provides an Executor Service interface. Executor Service was also introduced in JDK 1.5 with J.U.C.

public interface ExecutorService extends Executor

As you can see, Executor Service inherits Executor, which enhances task control on the basis of Executor, and includes the management of its own life cycle. There are four main categories:

  1. Close the executor and prohibit the submission of tasks.
  2. Monitor the status of the actuator;
  3. Provide support for asynchronous tasks;
  4. Provide support for batch tasks.
public interface ExecutorService extends Executor {

    /**
     * Closing the actuator has the following characteristics:
     * 1. Tasks that have been submitted to the executor will continue to be executed, but new tasks will no longer be submitted.
     * 2. If the executor is closed, there is no side effect in calling again.
     */
    void shutdown();

    /**
     * The main features of closing the actuator immediately are as follows:
     * 1. Attempts to stop all ongoing tasks are not guaranteed to stop successfully, but will try (for example, interrupt tasks through Thread.interrupt, but tasks that do not respond to interrupts may not terminate);
     * 2. Suspend processing of tasks submitted but not performed;
     *
     * @return Returns a list of tasks submitted but not executed
     */
    List<Runnable> shutdownNow();

    /**
     * If the executor is closed, return true.
     */
    boolean isShutdown();

    /**
     * Determine whether the executor has terminated.
     * <p>
     * true is returned only if the executor is closed and all tasks have been performed.
     * Note: This method always returns false unless shutdown or shutdown Now is called first.
     */
    boolean isTerminated();

    /**
     * Blocking the calling thread and waiting for the executor to reach the termination state.
     *
     * @return {@code true} If the executor finally reaches the termination state, it returns true; otherwise, it returns false.
     * @throws InterruptedException if interrupted while waiting
     */
    boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;

    /**
     * Submit a task with a return value for execution.
     * Note: Future's get method will return the return value of task when it is successfully completed.
     *
     * @param task Tasks to be submitted
     * @param <T>  The return value type of the task
     * @return Returns the Future object for the task
     * @throws RejectedExecutionException If the task cannot be scheduled for execution
     * @throws NullPointerException       if the task is null
     */
    <T> Future<T> submit(Callable<T> task);

    /**
     * Submit a Runnable task for execution.
     * Note: Future's get method will return the given result when it is successfully completed.
     *
     * @param task   Tasks to be submitted
     * @param result Return results
     * @param <T>    Return result type
     * @return Returns the Future object for the task
     * @throws RejectedExecutionException If the task cannot be scheduled for execution
     * @throws NullPointerException       if the task is null
     */
    <T> Future<T> submit(Runnable task, T result);

    /**
     * Submit a Runnable task for execution.
     * Note: Future's get method will return null when it completes successfully.
     *
     * @param task Tasks to be submitted
     * @return Returns the Future object for the task
     * @throws RejectedExecutionException If the task cannot be scheduled for execution
     * @throws NullPointerException       if the task is null
     */
    Future<?> submit(Runnable task);

    /**
     * Execute all tasks in a given set, and when all tasks are completed, return a Future list that maintains the status and results of the tasks.
     * <p>
     * Note: This method is a synchronization method. Future.isDone() of all elements in the return list is true.
     *
     * @param tasks Task set
     * @param <T>   Return result type of task
     * @return A list of Future objects for a task in the same order as the iterator generated in the set.
     * @throws InterruptedException       If an interruption occurs while waiting, all unfinished tasks will be cancelled.
     * @throws NullPointerException       Any task is null
     * @throws RejectedExecutionException If any task cannot be scheduled for execution
     */
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;

    /**
     * Execute all tasks in a given set, and return a Future list that maintains the status and results of tasks when all tasks are completed or when the timeout expires (whichever happens first).
     */
    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;

    /**
     * Execute tasks in a given set and return the result if only one of the tasks completes successfully first (no exception is thrown).
     * Once normal or abnormal returns, unfinished tasks are cancelled.
     */
    <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;

    /**
     * Execute tasks in a given set and return results if a task has been successfully completed (no exception thrown) before the expiration of a given timeout.
     * Once normal or abnormal returns, unfinished tasks are cancelled.
     */
    <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
About Future, it's actually in the Java multithreaded design pattern Future mode, we will talk about the Future framework in J.U.C.
The Future object provides support for asynchronous execution of tasks, that is, the calling thread does not have to wait for the task to complete, and after submitting the task to be executed, it immediately returns to the next execution. Then, you can check if Future has a result when you need it, and if the task has been completed, pass Future.get() method can get the execution result -- Future.get() is a blocking method.

Scheduled Executor Service

In an industrial environment, we may want to perform certain tasks submitted to the executor on a regular or periodic basis. At this time, we can implement the Executor interface ourselves to create classes that meet our needs. Doug Lea has taken this requirement into account, so it provides an interface on the basis of Executor Service. —— Scheduled Executor Service, which was also introduced in JDK 1.5 with J.U.C.

public interface ScheduledExecutorService extends ExecutorService

Scheduled Executor Service provides a series of schedule methods that can perform submitted tasks after a given delay or once per specified cycle. Let's look at the following example:

public class TestSche {
    public static void main(String[] args) {
        //Create a Scheduled ExecutorService instance
        ScheduledExecutorService executorService=new ScheduledThreadPoolExecutor(1);

        final ScheduledFuture<?>scheduledFuture=executorService.scheduleAtFixedRate(new BeepTask(),10,10
        ,TimeUnit.SECONDS);

        executorService.schedule(new Runnable() {
            @Override
            public void run() {
                scheduledFuture.cancel(true);
            }
        },1,TimeUnit.HOURS); //Cancel the task in one hour
    }

    private static class BeepTask implements Runnable{

        @Override
        public void run() {
            System.out.println("beep!...");
        }
    }
}

The above example first creates a Scheduled Executor Service type executor, and then submits a "buzzing" task using the scheduleAtFixedRate method, which is executed every 10 seconds.

Note: The schedule AtFixedRate method returns a Scheduled Future object. Scheduled Future actually adds latency to Future. With Scheduled Future, you can cancel the execution of a task. In this case, we use the scheme method to set the cancellation of the execution task after one hour.

The complete interface declaration of Scheduled Executor Service is as follows:

public interface ScheduledExecutorService extends ExecutorService {
 
    /**
     * Submit a task to be performed and execute it after a given delay.
     *
     * @param command Tasks to be performed
     * @param delay   delay time
     * @param unit    Unit of delay time
     */
    public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit);
 
    /**
     * Submit a task to be performed (with a return value) and execute it after a given delay.
     *
     * @param command Tasks to be performed
     * @param delay   delay time
     * @param unit    Unit of delay time
     * @param <V>     return type
     */
    public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit);
 
    /**
     * Submit a task to be performed.
     * This task starts after initial Delay, then after initial Delay + period, then after initial Delay + 2 * period, and so on.
     *
     * @param command      Tasks to be performed
     * @param initialDelay Delay time for first execution
     * @param period       Periods between successive executions
     * @param unit         Unit of delay time
     */
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit);
 
    /**
     * Submit a task to be performed.
     * The task starts after initial Delay, and then there is a given delay between each execution termination and the next execution start.
     * If any execution of a task encounters an exception, subsequent execution is cancelled. Otherwise, the task can only be terminated by canceling or terminating the executing program.
     *
     * @param command      Tasks to be performed
     * @param initialDelay Delay time for first execution
     * @param delay        Delay between one execution termination and the next execution start
     * @param unit         Unit of delay time
     */
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit);
}

So far, the three core interfaces in Executors framework have been introduced. The relationships among these three interfaces are as follows:

2. The factories that produce executor s

Through the first part of the study, the reader should have a preliminary understanding of the Executors framework, which is used to decouple the task itself from the task execution, and provide three core interfaces to meet user needs:

  1. Executor: Submit common executable tasks
  2. ExecutorService: Provides support for thread pool lifecycle management, asynchronous tasks
  3. Scheduled Executor Service: Provides support for periodic execution of tasks

Since the above three kinds of actuators are only interfaces, there must be specific implementation classes. J.U.C provides many default interface implementations. If users want to create instances of these classes themselves, they need to know the details of these classes. Is there a direct way to create them just according to some required features (parameters)? What about building these examples? Because for users, only these three interfaces are used.

In JDK 1.5, an Executors class is also provided in J.U.C. It is used to create the implementation class object of the above interface. Executors is actually a simple factory. All its methods are static. Users can choose the Executor instances they need to create according to their needs. Executors provides five types of Executor instances that can be created.

Thread pool with fixed number of threads

Executors provides two ways to create Executors with fixed threads. The fixed thread pool determines the total number of threads in them at initialization and keeps the number of threads unchanged throughout operation.

You can see that both of the following creation methods actually return a ThreadPoolExecutor instance. ThreadPoolExecutor is an implementation class of the ExecutorService interface. We will explain it later in a special chapter. Now we just need to understand that this is an Executor, which is used to schedule the execution of threads in it.

/**
 * Create an Executor with a fixed number of threads.
 */
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>());
}

/**
 * Create an Executor with a fixed number of threads.
 * Create new threads when needed using the provided ThreadFactory.
 */
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS,
            new LinkedBlockingQueue<Runnable>(), threadFactory);

}

Note above that the ThreadFactory interface:

public interface ThreadFactory {
    Thread newThread(Runnable r);
}

Since the return is a thread pool, it involves thread creation. Generally, we need to create a new thread through the new Thread () method, but we may want to set some thread attributes, such as
Name, daemon status, ThreadGroup, etc. There are many threads in the thread pool. If each thread is configured manually in this way, it will be very cumbersome. ThreadFactory, as a thread factory, can free us from these tedious thread status settings, and can also be specified externally by ThreadFactor. Y instance to determine how to create threads.

Executors provides static internal classes and implements the ThreadFactory interface. The simplest and most commonly used one is the DefaultThreadFactory below.

/**
 * Default thread factory.
 */
static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;
 
    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
    }
 
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

As you can see, when DefaultThreadFactory is initialized, thread groups, thread names and other information are defined. Each thread is created, these information is uniformly allocated to threads, avoiding the manual creation of threads through new mode, and reusing the factory.

Thread pool for a single thread

In addition to a fixed thread pool, Executors provides two ways to create Executors with only a single thread:

/**
 * Create an Executor using a single worker thread.
 */
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}
 
/**
 * Create an Executor using a single worker thread.
 * Create new threads when needed using the provided ThreadFactory.
 */
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>(), threadFactory));
}

As you can see, a thread pool with only one thread is actually a fixed thread pool with a specified number of threads. The main difference is that the returned Executor instance is wrapped with a Finalizable Delegated Executor Service object.

Let's look at Finalizable Delegated Executor Service, which defines only one finalize method:

static class FinalizableDelegatedExecutorService
        extends DelegatedExecutorService {
        FinalizableDelegatedExecutorService(ExecutorService executor) {
            super(executor);
        }
        protected void finalize() {
            super.shutdown();
        }
    }

The core is its inherited Delegated ExecutorService, which is a wrapper class that implements all methods of ExecutorService, but the internal implementation is actually delegated to the incoming ExecutorService instance:

/**
 * ExecutorService Implementation class wrapper class.
 */
    /**
     * A wrapper class that exposes only the ExecutorService methods
     * of an ExecutorService implementation.
     */
    static class DelegatedExecutorService extends AbstractExecutorService {
        private final ExecutorService e;
        DelegatedExecutorService(ExecutorService executor) { e = executor; }
        public void execute(Runnable command) { e.execute(command); }
        public void shutdown() { e.shutdown(); }
        public List<Runnable> shutdownNow() { return e.shutdownNow(); }
        public boolean isShutdown() { return e.isShutdown(); }
        public boolean isTerminated() { return e.isTerminated(); }
        public boolean awaitTermination(long timeout, TimeUnit unit)
            throws InterruptedException {
            return e.awaitTermination(timeout, unit);
        }
        public Future<?> submit(Runnable task) {
            return e.submit(task);
        }
        public <T> Future<T> submit(Callable<T> task) {
            return e.submit(task);
        }
        public <T> Future<T> submit(Runnable task, T result) {
            return e.submit(task, result);
        }
        public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
            throws InterruptedException {
            return e.invokeAll(tasks);
        }
        public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                             long timeout, TimeUnit unit)
            throws InterruptedException {
            return e.invokeAll(tasks, timeout, unit);
        }
        public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
            throws InterruptedException, ExecutionException {
            return e.invokeAny(tasks);
        }
        public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                               long timeout, TimeUnit unit)
            throws InterruptedException, ExecutionException, TimeoutException {
            return e.invokeAny(tasks, timeout, unit);
        }
    }
Why do we need to add such a delegation? Because the ThreadPool Executor returned contains methods to set the size of the thread pool, such as setCorePool Size, we don't want users to use these methods in a forced way for a single thread pool, so we need a wrapper class that exposes only the ExecutorService itself.

Cacheable thread pool

In some cases, although we have created thread pools with a certain number of threads, we may want to recycle threads at a specific time (for example, threads are not used beyond a specified time) for resource utilization reasons. Executors provides this type of thread pool:

/**
 * Create an Execotor that can cache threads.
 * If no thread is available in the thread pool, a new thread is created and added to the pool.
 * If threads are not used for a long time (default 60s, configurable through threadFactory), they are removed from the cache.
 */
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>());
}
 
/**
 * Create an Execotor that can cache threads.
 * If no thread is available in the thread pool, a new thread is created and added to the pool.
 * If threads are not used for a long time (default 60s, configurable through threadFactory), they are removed from the cache.
 * Create new threads when needed using the provided ThreadFactory.
 */
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
            new SynchronousQueue<Runnable>(), threadFactory);
}

As you can see, the ThreadPoolExecutor object is returned, only the timeout time is specified, and the number of threads in the thread pool is between [0, Integer.MAX_VALUE].

Delay/Periodic Scheduling Thread Pool

If a task needs to be delayed/periodically invoked, it needs to return an instance of the Scheduled Executor Service interface, which is an Executor that implements the Scheduled Executor Service interface, just like the ThreadPool Executor, which we will explain later.

/**
 * Create a schedulable Executor with a fixed number of threads.
 * It can schedule tasks to execute after specified delays or periodically.
 */
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
 
/**
 * Create a schedulable Executor with a fixed number of threads.
 * It can schedule tasks to execute after specified delays or periodically.
 * Create new threads when needed using the provided ThreadFactory.
 */
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

Fork/Join Thread Pool

Fork/Join thread pool is a special kind of thread pool, which was introduced only in JDK 1.7. Its core implementation is ForkJoin Pool class. As for the Fork/Join framework, we'll talk about it later. Now just know that the Executors framework provides a convenient way to create this kind of thread pool.

/**
 * Create a ForkJoin thread pool with the specified parallel level.
 */
public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool(parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
}
 
/**
 * Create a ForkJoin thread pool with parallel level equal to CPU core number.
 */
public static ExecutorService newWorkStealingPool() {
    return new ForkJoinPool(Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory,
            null, true);
}

Summary

At this point, the overall structure of Executors framework is basically explained. At this time, we should have in mind a class inheritance map as follows:

Let's review the relationships and roles of the above interfaces/classes:

  1. Executor
    Actuator interface is also the top-level abstract core interface, which separates the task from the task execution.
  2. ExecutorService
    On the basis of Executor, it provides the functions of life cycle management and asynchronous task execution.
  3. ScheduledExecutorService
    On the basis of Executor Service, the function of delayed execution/periodic execution of tasks is provided.
  4. Executors
    Static Factory for Producing Specific Actuators
  5. ThreadFactory
    Thread factories are used to create individual threads, reduce the tedious work of creating threads manually, and reuse the characteristics of factories.
  6. AbstractExecutorService
    The Abstract implementation of ExecutorService provides the basis for the implementation of various types of executor classes.
  7. ThreadPoolExecutor
    Thread pool Executor, also the most commonly used Executor, can manage threads in the way of thread pool.
  8. ScheduledThreadPoolExecutor
    On the basis of ThreadPool Executor, support for periodic task scheduling is added.
  9. ForkJoinPool
    Fork/Join thread pool, introduced at JDK 1.7, implements the core class of Fork/Join framework.

About ThreadPool Executor and Scheduled ThreadPool Executor, we will explain in detail in the next chapter to help readers understand the implementation principle of thread pool. As for Fork Join Pool, we will introduce the parallel framework of Fork/Join later.

Reference to this article

https://segmentfault.com/a/1190000016586578#articleHeader10

Posted by jbrave on Tue, 23 Jul 2019 20:58:33 -0700