Java thread pool understanding summary

Basic principle of thread pool

ThreadPoolExecutor

Executors is the factory class of java thread pool. It can quickly initialize a thread pool that meets the business requirements. Its essence is to initialize a ThreadPoolExecutor object with different parameters. The specific parameters are described as follows:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
  • corePoolSize
    The number of core threads in the thread pool. When a task is submitted, the thread pool creates a new thread to execute the task until the current thread number is equal to corePoolSize. If the current thread number is corePoolSize, the continuing submitted task is saved in the blocking queue and waiting to be executed. If the prestartAllCoreThreads() method of the thread pool is executed, the thread pool will be created and started in advance All core threads. By default, you can live forever.
  • maximumPoolSize
    The maximum number of threads allowed in the thread pool. If the current blocking queue is full and the task continues to be submitted, a new thread is created to execute the task, provided that the current number of threads is less than maximumPoolSize;
  • keepAliveTime
    The survival time when a thread is idle, that is, the time when the thread continues to live when there is no task to execute; by default, this parameter is only useful when the number of online processes is greater than corePoolSize;
  • unit
    The unit of keepAliveTime;
  • workQueue
    It is used to save the blocking queue of the task waiting to be executed, and the task must implement the Runable interface. The following blocking queue is provided in the JDK:

    • ArrayBlockingQueue: Based on the bounded blocking queue of array structure, sort tasks by FIFO;
    • Linkedblockingqueue: Based on the block queue of linked list structure, tasks are sorted by FIFO, and the throughput is usually higher than arrayblockingqueue;
    • SynchronousQuene: a blocking queue that does not store elements. Each insertion must wait until another thread calls the removal operation. Otherwise, the insertion operation is always blocked. The throughput is usually higher than that of linkedblockingqueue;
    • Priorityblockingqueue: an unbounded blocking queue with priority;
  • threadFactory
    Create a thread factory, and set a thread name with recognition degree for each new thread through the customized thread factory.
  • handler
    The saturation strategy of thread pool. When the blocking queue is full and there are no idle worker threads, if you continue to submit a task, you must adopt a strategy to handle the task. The thread pool provides four strategies:

    • AbortPolicy: throw an exception directly, default policy;
    • CallerRunsPolicy: use the thread of the caller to execute the task;
    • DiscardOldestPolicy: discards the top task in the blocking queue and executes the current task;
    • DiscardPolicy: discard the task directly;
    • Of course, the RejectedExecutionHandler interface can also be implemented according to the application scenario, and saturation policies can be customized, such as logging or tasks that cannot be processed by persistent storage.

Executors 4 thread pools

  • newFixedThreadPool
    Create a fixed length thread pool, which can control the maximum concurrent number of threads, and the exceeding threads will wait in the queue.
public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
        }

Linkedblockingqueue's default maximum is integer.max'value

  • newSingleThreadExecutor
    Create a single threaded pool, which only uses unique worker threads to execute tasks, ensuring that all tasks are executed in the specified order (FIFO, LIFO, priority).
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
  • newCachedThreadPool
    Create a cacheable thread pool. If the length of the thread pool exceeds the processing requirements, idle threads can be recycled flexibly. If there is no recyclable thread, a new thread can be created.
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
  • newScheduledThreadPool
    Create a thread pool that supports timed and periodic task execution. In most cases, it can be used to replace Timer class.
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

[note] it is not recommended to use Executors to create threads

Implementation principle of thread pool

Working process of thread pool

  1. When the thread pool was created, there was no thread in it. The task queue is passed in as a parameter. However, even if there are tasks in the queue, the thread pool will not execute them immediately.
  2. When calling the execute() method to add a task, the thread pool will make the following judgment:

    • If the number of running threads is less than corePoolSize, create a thread to run the task immediately;
    • If the number of running threads is greater than or equal to corePoolSize, put the task into the queue;
    • If the queue is full at this time, and the number of running threads is less than the maximumPoolSize, create a non core thread to run the task immediately;
    • If the queue is full and the number of running threads is greater than or equal to maximumPoolSize, the thread pool throws an exception RejectExecutionException.
  3. When a thread completes a task, it takes a task from the queue to execute.
  4. When a thread has nothing to do and exceeds a certain time (keepAliveTime), the thread pool will judge that if the current number of running threads is greater than corePoolSize, the thread will be stopped. So when all the tasks of thread pool are completed, it will eventually shrink to the size of corePoolSize.

Source code interpretation

Among them, the function of AtomicInteger variable ctl is very powerful: use the low 29 bits to represent the number of threads in the thread pool, and use the high 3 bits to represent the running state of the thread pool:

  • RUNNING: - 1 < count \ bits, i.e. the highest 3 bits are 111. The thread pool in this state will receive new tasks and process tasks in the blocking queue;
  • SHUTDOWN: 0 < count ﹤ bits, i.e. the highest 3 bits are 000. The thread pool in this state will not receive new tasks, but will process tasks in the blocking queue;
  • STOP: 1 < count ﹤ bits, i.e. the high 3 bits is 001, the thread in this status will not receive new tasks, nor process tasks in the blocking queue, and will interrupt the running tasks;
  • TIDYING: 2 < count ﹤ bits, i.e. the high 3 bits are 010;
  • TERMINATED: 3 < count ﹤ bits, i.e. the high 3 bits are 011;
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        // Current number of threads < corepoolsize
        if (workerCountOf(c) < corePoolSize) {
            // Start a new thread directly
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // Number of active threads > = corepoolsize
    // runState is running & & queue is not full
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // Check whether it is RUNNING again
               // Non RUNNING status removes the task from workQueue and rejects it
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        // Two situations:
        // 1. Reject new task in non RUNNING state
        // 2. Failed to start a new thread when the queue is full (workcount > maximumpoolsize)
        else if (!addWorker(command, false))
            reject(command);
}

Google Guava concurrent

java Future

public interface Future<V> {

    /**
     * Attempts to cancel execution of this task.  This attempt will
     * fail if the task has already completed, has already been cancelled,
     * or could not be cancelled for some other reason. If successful,
     * and this task has not started when {@code cancel} is called,
     * this task should never run.  If the task has already started,
     * then the {@code mayInterruptIfRunning} parameter determines
     * whether the thread executing this task should be interrupted in
     * an attempt to stop the task.
     *
     * <p>After this method returns, subsequent calls to {@link #isDone} will
     * always return {@code true}.  Subsequent calls to {@link #isCancelled}
     * will always return {@code true} if this method returned {@code true}.
     *
     * @param mayInterruptIfRunning {@code true} if the thread executing this
     * task should be interrupted; otherwise, in-progress tasks are allowed
     * to complete
     * @return {@code false} if the task could not be cancelled,
     * typically because it has already completed normally;
     * {@code true} otherwise
     */
    boolean cancel(boolean mayInterruptIfRunning);

    /**
     * Returns {@code true} if this task was cancelled before it completed
     * normally.
     *
     * @return {@code true} if this task was cancelled before it completed
     */
    boolean isCancelled();

    /**
     * Returns {@code true} if this task completed.
     *
     * Completion may be due to normal termination, an exception, or
     * cancellation -- in all of these cases, this method will return
     * {@code true}.
     *
     * @return {@code true} if this task completed
     */
    boolean isDone();

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     */
    V get() throws InterruptedException, ExecutionException;

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result, if available.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the timeout argument
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     * @throws TimeoutException if the wait timed out
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

The get() method can return a result when the task is finished. If the work is not finished when the call is made, the thread will be blocked until the task is finished
get (long timeout,TimeUnit unit)
The cancel (boolean mayInterruptIfRunning) method can be used to stop a task. If the task can be stopped (judged by mayInterruptIfRunning), it can return true. If the task has been completed or stopped, or the task cannot be stopped, it will return false
The isDone () method determines whether the current method is complete
isCancel() method determines whether the current method is cancelled

guava ListenableFuture

Guava defines ListenableFuture to extend the Future interface. Allows callback methods to be registered

Futures.addCallback(ListenableFuture<V>,FutureCallback<V>, Executor)

At the same time, the Future extension in Guava includes:

  • transform: converts the return value of ListenableFuture.
  • allAsList: for the combination of multiple listenablefeatures, return a List object composed of multiple Future return values when all Future are successful. Note: when one of the Future fails or is cancelled, it will enter failure or cancellation.
  • Successful aslist: similar to allAsList, the only difference is that the Future return value of failure or cancellation is replaced by null. No failure or cancellation process will be entered.
  • immediateFuture/immediateCancelledFuture: immediately returns a ListenableFuture of the value to be returned.
  • makeChecked: converts ListenableFuture to CheckedFuture. CheckedFuture is a ListenableFuture, which contains multiple versions of get methods. Method declarations throw check exceptions. This makes it easier to create a Future that can throw exceptions in the execution logic
  • JdkFutureAdapters.listenInPoolThread(future): guava also provides an interface function to convert JDK Future to ListenableFuture.

java CompletableFuture

How to create an asynchronous operation

public static CompletableFuture<Void> runAsync(Runnable runnable)
public static CompletableFuture<Void> runAsync(Runnable runnable,Executor executor)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier,Executor executor)
  • runAsync is similar to execute method and does not support return value, while supplyAsync method is similar to submit method and supports return value. It is also our key method.
  • A method that does not specify an Executor uses ForkJoinPool.commonPool() as its thread pool to execute asynchronous code.

Continuous asynchronous operation

// This method is similar to thenAccept, and is generally used for the last execution of the callback function. However, this method does not accept the return value of the callback function, which simply represents the last step of executing the task
public CompletableFuture<Void> thenRun(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action)
public CompletableFuture<Void> thenRunAsync(Runnable action,Executor executor)

// Through this method, the chain transformation can be carried out many times and the final machining results can be returned.
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn) 
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)

// This method can accept a return value of Futrue, but it does not return any value. It is suitable for the last step of multiple callback functions.
public CompletableFuture<Void> thenAccept(Consumer<? super T> action) 
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action) 
public CompletableFuture<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor) 

Wait for the operation to complete

public CompletableFuture<T> whenComplete(BiConsumer<? super T, ? super Throwable> action) 
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action) 
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T, ? super Throwable> action, Executor executor)

These methods are all executed after the asynchronous task created above is completed (or it may end after an exception is thrown). The difference between whenComplete and whenCompleteAsync is that the former is executed by the above thread, and the latter is to leave the task of whenCompleteAsync to the thread pool for decision.

combination

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn) 
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) 
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn,Executor executor) 
public <U,V> CompletableFuture<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn) 
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn) 
public <U,V> CompletableFuture<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn, Executor executor)

We often need to merge the results of two tasks, and process them uniformly. In short, the callback task here needs to wait for the completion of both tasks before triggering

Result & exception handling

public <U> CompletableFuture<U> handle(BiFunction<? super T, Throwable, ? extends U> fn) 
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn) 
public <U> CompletableFuture<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn, Executor executor) 
public CompletableFuture<T> exceptionally(Function<Throwable, ? extends T> fn) 
  • If there is A dependency between two tasks, handle and then apply can be used. The effect is the same. The difference is that when there is an exception in the execution of task A, the thenApply method will not execute, and the handle method will execute. Because in the handle method, we can handle the exception, but the former can't.
  • exceptionally is only executed when there is an exception.

allOf

public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)

In many cases, there are more than two asynchronous tasks, maybe tens or hundreds. We need to wait for these tasks to be completed before performing the corresponding operations. How to monitor whether all tasks are finished or not? The allOf method can help us.

Practical summary of thread pool

Thread pool creation

Use Guava ThreadFactoryBuilder to set the name of the thread pool

        ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat(
                "my-test-pool-%d").build();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize,
                maximumPoolSize, keepAliveTime, TimeUnit.SECONDS,
                new LinkedBlockingDeque<>(DEFAULT_QUEUE_CAPACITY), namedThreadFactory);

Thread pool monitoring

Thread information statistics

For thread pool monitoring, the key information of thread pool is obtained through ThreadPoolExecutor. These key data can be counted regularly by writing help class:

  • queue.size: gets the number of thread pool task queues.
  • taskCount: the number of tasks that the thread pool needs to perform.
  • completedTaskCount: the number of tasks completed by the thread pool during the run. Less than or equal to taskCount.
  • largestPoolSize: the maximum number of threads that the thread pool has ever created. Through this data, we can know whether the thread pool is full. If it is equal to the maximum size of the thread pool, it means that the thread pool has been full.
  • getPoolSize: number of threads in the thread pool. If the thread pool is not destroyed, the threads in the pool will not be destroyed automatically, so this size will only increase or not decrease.
  • getActiveCount: gets the number of active threads.
  • maximumPoolSize: maximum number of threads

Rewrite ThreadPoolExecutor to realize thread pool monitoring

By rewriting the beforeExecute, afterExecute, and terminated methods of ThreadPoolExecutor, you can do something before, after, and before the thread pool is closed. For example, the average execution time, the maximum execution time and the minimum execution time of monitoring tasks.

Reference document

Tags: Java less JDK Google

Posted on Tue, 18 Feb 2020 23:44:45 -0500 by jjk-duffy