Java Concurrency Tool Learning - Thread Pool

Preface

Almost all of the previous sections on the Java multithreading basics are summarized. Starting with this blog, let's summarize the contents of Java concurrency tools, starting with thread pools

Why there is a thread pool

There are two main reasons: 1. Repeatedly creating threads is expensive (which I really don't want to explain); 2. Too many threads will consume too much memory

To address these two issues, Java introduced the concept of pools.

Use a small number of threads to avoid the hassle of consuming too much memory; Always keep only some threads working and can repeat the submitted tasks, which avoids the wastage of frequent thread switching.

Thread pools can speed up response to some extent, make reasonable use of CPU and memory, and facilitate the unified management of resources. In practical development, if more than five threads need to be created, we should generally consider using thread pools to achieve this.

Thread pool parameters and task addition rules

Creating a thread pool is slightly more cumbersome, has more parameters, and involves more concepts than creating a single thread. Summarize step by step

Parameters for Thread Pool Constructor

First, familiarize yourself with the parameters used to create the thread pool, perhaps by sweeping it, some of which may not be very interesting.

Parameter NametypeMeaning
corePoolSizeintNumber of Core Threads
maxPoolSizeintMaximum Threads
keepAliveTimelongThread Lifetime
workQueueBlockingQueueQueue to store tasks
threadFactoryThreadFactoryGenerate a new thread's factory class
handlerRejectedExecutionHandlerThread pool rejection policy

corePoolSize is the number of core threads. After initialization, there are no threads in the thread pool. The thread pool waits for tasks to arrive before creating threads (taking tasks out of the task queue and creating threads afterwards).

maxPoolSize has a maximum number of threads, and one of the advantages of a thread pool is that you can flexibly expand the number of active threads you have based on how many tasks you have. The number of tasks to be executed at different times in the same thread pool is also different, because the number of core threads is not enough to handle too many tasks, at which point we need to extend the threads that process tasks in the thread pool

KeepAliveTime If the thread pool currently has more threads than corePoolSize, they will be terminated if the extra thread is idle longer than keepAliveTime

The workQueue stores the queue of tasks. There are usually three kinds of queues to store tasks: 1. SynchronousQueue, the directly handed-over task queue; 2. Unbounded Queue - LinkedBlockingQueue; 3. Bounded Queue--ArrayBlockingQueue

ThreadFactory new threads are created by ThreadFactory, using Executors.defaultThreadFactory() by default, and create threads in the same thread group with the same NORM_PRIORITY priority, and none of them are daemon threads. If you specify the ThreadFactory yourself, you can change properties such as thread name, thread group, priority, whether it is a daemon thread, and so on, but this is not usually necessary.

handler rejection policy, when the maximum number of thread pools is reached and the task queue is full, the newly submitted task will be rejected, and the rejection policy will execute the specific rejection action

Rules for adding tasks

It doesn't make much sense to memorize these parameters separately to create a thread pool. We need attributes and understand the rules for adding tasks to a thread pool to concatenate them

Thread pools have rules for adding tasks because they are flexible to extend

1. If the number of threads in the current thread pool is less than corePoolSize, the thread pool will still create a thread to run new tasks even if other worker threads are idle.

2. If the number of threads is equal to (or greater than) corePoolSize but less than maxPoolSize, put the task in the queue first, and the number of active threads in the thread pool is the value of corePoolSize

3. If the queue is full at this time and the number of threads in the thread pool is less than maxPoolSize, the thread pool creates a new thread to run the task

4. If the queue is full and the number of threads in the thread pool has reached maxPoolSize, the thread pool will execute the rejection policy directly if there are tasks committed to the thread pool outside at this time.

5. If the number of tasks in the thread pool is small and there are many active but idle threads at this time, those idle threads will be terminated if they are idle longer than the keepAliveTime set.

A diagram of rules for adding tasks to a thread pool

You can see that the order of judgment is corePoolSize->workQueue->maxPoolSize.

Take a simple example:

If the number of core threads in a thread pool is set to 5, the maximum number of threads is 10, and the queue length is 100.

At first, there weren't many threads, three tasks came, and the thread pool created three threads to handle the three tasks submitted. Later tasks continue to be added to the thread pool, where they are processed first with five core threads. If they cannot be processed, they are temporarily stored in the task queue. If, over time, the task queue is full and subsequent tasks are still submitted, the thread pool will continue to create more threads to process tasks. If the number of active threads exceeds the maximum number of threads set at 10, the thread pool rejects processing subsequent submitted tasks.

Based on the rules that thread pools handle tasks, summarize them further:

1. If we set the corePoolSize to be the same as the maxPoolSize, can we create a thread pool with a fixed number of active threads?

2. Thread pools generally want to maintain a small number of active threads, only when the load (the number of tasks in the task queue) becomes large.

3. If maxPoolSize is set to a high value (such as Integer.MAX_VALUE), in theory, the thread pool can add many new threads to handle tasks.

4. The thread pool will create more active threads only when the task queue is full. If we use an unbounded queue (such as LinkedBlockingQueue) to store tasks, that is, the task queue can never be full, then the number of active threads in the thread pool cannot exceed corePoolSize at this time.

So according to the characteristics of thread pools, JDK customizes thread pools for some threads, each of which has its own characteristics.

Some thread pools provided by JDK

JDK actually provides us with some commonly used thread pools, each of which has its own characteristics. Let's start with some combing

newFixedThreadPool

Its constructor source code is as follows

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    //The first parameter is corePoolSize, the second parameter is maxPoolSize, and the third and fourth parameters are keepAliveTime and its units
    //The fourth parameter is the task queue, and the fifth parameter is the ThreadFactory
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

You can see that this is a large thread pool such as corePoolSize and maxPoolSize, and its task queue is an unbounded task queue. Therefore, when there are too many tasks, the number of threads in this thread pool is fixed, so it is called FixedThreadPool.

Basic use cases

/**
 * autor:liman
 * createtime:2021-11-01
 * comment: newFixThreadPool Examples
 */
public class NewFixThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        for(int i=0;i<1000;i++){
            executorService.execute(new Task());
        }
    }

}

class Task implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"End of run");
    }
}

Because the number of active threads in this thread pool is fixed when there are too many tasks, and there is no capacity limit on the task queue, when requests are increasing and cannot be processed in time, it is easy to cause a large amount of memory to be consumed when requests are piled up, which may lead to OOM

For example, if you set the JVM parameter a little smaller, you will get an OOM exception

/**
 * autor:liman
 * createtime:2021-11-01
 * comment: newFixedThreadPool An instance of OOM appears
 * JVM Parameter-Xmx8m-Xms8m
 */
public class FixThreadPoolOOM {

    private static ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
      for(int i=0;i<Integer.MAX_VALUE;i++){
          executorService.execute(new OOMTask());
      }
    }

}

class OOMTask implements Runnable{

    @Override
    public void run() {
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

newSingleThreadExecutor

Constructor Source

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    //You can see that the corePoolSize and maxPoolSize parameters are set to 1 only on the basis of newFixedThreadPool
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

You can see that only on the basis of newFixedThreadPool, the corePoolSize and maxPoolSize parameters are set to 1. There is a problem with newFixedThreadPool, which is a thread pool with a simple understanding of a low-profile version of newFixedThreadPool.

newCachedThreadPool

A thread pool that can be cached, is a boundless thread pool, and has the ability to automatically recycle extra threads. Its constructor source code is as follows

public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    //corePoolSize is set to 0 and maxPoolSize to Integer.MAX_VALUE
    //The task queue is using SynchronousQueue
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

As you can see from the constructor, its corePoolSize is set to 0, its maxPoolSize is set to the maximum of the integer, and its task queue is a SynchronousQueue, which we mentioned earlier is a directly handed-over queue with an internal capacity of 0. Task queues do not essentially store tasks, but are handed directly to threads for execution, so the thread pool needs to constantly create threads to handle submitted tasks.

Over time, idle threads that do not have tasks are recycled.

Instance Code

/**
 * autor:liman
 * createtime:2021-11-01
 * comment:
 */
public class CacheThreadPoolDemo {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        for(int i=0;i<1000;i++){
            executorService.execute(new CacheThreadTask());
        }
    }
}

class CacheThreadTask implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

Unlike other thread pools, there are a few more threads working on tasks here

OOM also occurs in this thread pool because new threads are created every time a task comes in.

newScheduledThreadPool

Thread pool supporting periodic or periodic task execution

One of the constructor sources

public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    //I don't want to introduce it anymore.
    //Task queues use delayed queues
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}

Basic use cases

/**
 * autor:liman
 * createtime:2021-11-01
 * comment:scheduledThreadPool Examples
 */
public class ScheduledThreadPoolDemo {

    public static void main(String[] args) {
        ScheduledExecutorService executorService
                = Executors.newScheduledThreadPool(10);
        //Delay execution of specified task by five seconds
        //executorService.schedule(new ScheduledTask(),5, TimeUnit.SECONDS);
        //Start the first time, execute the task after 1 second, then execute it every 3 seconds
        executorService.scheduleAtFixedRate(new ScheduledTask(),1,3,TimeUnit.SECONDS);
    }
}
class ScheduledTask implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}

There are other thread pools added to JDK1.8, but they are not particularly common, so no summary is made here.

Automatic or manual creation

As for whether thread pools are created automatically or manually, when you come here, you should have an answer. Although JDK provides some available thread pools, they have some drawbacks and are highly designed. If our business scenario fits this own thread pool, you can actually try it. However, in most cases it is recommended that we create the thread pool manually.

How to set the number of threads in the thread pool

1. If our program is CPU intensive, such as encryption and frequent hash operations, the optimal number of threads at this time is 1-2 times the number of CPU cores.

2. If our program is IO-type, such as frequent reading and writing files, frequent network transfers, etc., the optimal number of threads at this time will generally be many times larger than the number of CPU cores, based on the busy situation displayed by JVM thread monitoring, to ensure that the idle threads can be converged.

The general formula is:
Line Check number = C P U nucleus heart number ∗ ( 1 + flat all etc. stay time between / flat all work do time between ) Number of threads = Number of CPU cores* (1 + Average wait time / Average working time) Number of threads = Number of CPU cores (1 + Average wait time / Average working time)
The so-called average wait time is only the time a thread spends waiting for data, and the average working time is the time a thread spends processing data.

Stop Thread Pool Correctly

Stopping the thread pool is easier than stopping individual threads normally.

Related methods

shutdown

A direct call to the shutdown method of the thread pool is equivalent to simply telling the thread pool an interrupt signal that the thread pool will wait for the task in the queue and that the task being executed in the active thread will be executed before it stops. However, during the period when the thread pool responds to an interrupt signal, submitting tasks to the thread pool will result in an error.

Instance Code

/**
 * autor:liman
 * createtime:2021-11-02
 * comment:
 */
public class ShutDownDemo {

    public static void main(String[] args) throws InterruptedException {
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 1000; i++) {
            fixedThreadPool.execute(new ShutDownTask());
        }
        Thread.sleep(1500);
        //By calling shutdown here, you can see that the thread pool is still not slow enough to finish processing its own tasks before stopping
        fixedThreadPool.shutdown();
        fixedThreadPool.execute(new ShutDownTask());//Exception thrown here, thread pool does not accept new tasks when handling interrupts
    }

}

class ShutDownTask implements Runnable {

    @Override
    public void run() {
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName());
    }
}

isShutdown

Determines if thread pool eating is processing interrupts.

isTerminated

Determine if the thread pool is completely stopped

awaitTermination

Determines if the thread pool stops completely after a period of time, returns true if it stops completely, or false if it does not

shutDownNow

Close immediately. Compare violence to shut down ongoing tasks directly.

If we want to stop the thread pool normally, it is recommended that we use the judgment to stop instead of using the last method to stop the thread pool directly and violently.

Rejection Policy

Rejection Policies There are not many examples to illustrate here, just a summary of several rejection strategies

Rejection PolicyRejection Method
AbortPolicyThrow an exception directly and it will not be submitted successfully
DiscardPolicyThread pool silently discards tasks
DiscardOldestPolicyDrop the oldest tasks in the queue
CallerRunsPolicyThreads submitting tasks execute rejection policies

Developer-defined rejection policies are also supported, simply by implementing an interface to the corresponding rejection policy

Thread Pool Principle and Source Code Analysis

In fact, this summary almost covers a few common points of knowledge in thread pools, and here we will briefly review the source code and the principles.

A thread pool consists of a thread pool manager, a work queue, a task queue, and a task interface.

Relationships between classes

Executor, ThreadPoolExecutor, Executor Service, Executors and so on. Let's first sort through the relationships

Just look at the picture first

Executor is a top-level interface where there is only one method

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @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);
}

ExecutorService is an interface that inherits Executor, with new methods such as closing thread pools added

Executors is a tool class, just a tool class for creating a thread pool

ThreadPoolExecutor is the thread pool subclass, but we created the thread pool through Executors and returned the class ExecutorService, which is the parent of ThreadPoolExecutor

Principle of Task Reuse by Thread Pool

Enter from executor method

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    //Comments in the associated source code also describe the steps for thread pools to perform tasks and extend core threads
    /*
     * Proceed in 3 steps:
     *
     * 1. If fewer than corePoolSize threads are running, try to
     * start a new thread with the given command as its first
     * task.  The call to addWorker atomically checks runState and
     * workerCount, and so prevents false alarms that would add
     * threads when it shouldn't, by returning false.
     *
     * 2. If a task can be successfully queued, then we still need
     * to double-check whether we should have added a thread
     * (because existing ones died since last checking) or that
     * the pool shut down since entry into this method. So we
     * recheck state and if necessary roll back the enqueuing if
     * stopped, or start a new thread if there are none.
     *
     * 3. If we cannot queue task, then we try to add a new
     * thread.  If it fails, we know we are shut down or saturated
     * and so reject the task.
     */
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        //If the number of currently running threads is less than the number of core threads, a thread is created, where the second parameter of addWorker is true to indicate whether the number of currently active threads is greater than the number of core threads when creating a thread, and false to indicate whether the number of running threads is greater than maxPoolSize when creating a thread
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    //Check if the thread pool is running and, if so, place the task in a waiting queue
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        //If the thread is not running, delete the current task and execute the rejection policy
        if (! isRunning(recheck) && remove(command))
            reject(command);//Execute Rejection Policy
        else if (workerCountOf(recheck) == 0)//If no thread is currently executing a task, you need to create a thread
            addWorker(null, false);
    }
    //If addWorker returns false, it means it has not been added to the task queue and will be rejected
    else if (!addWorker(command, false))
        reject(command);
}

The source code is explained in the code comments, so let's continue with addWorker

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (int c = ctl.get();;) {
        // Check if queue empty only if necessary.
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;

        for (;;) {
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                // Recheck while holding lock.
                // Back out on ThreadFactory failure or if
                // shut down before lock acquired.
                int c = ctl.get();

                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    //Build a Worker object from the incoming Runnable and add it to the task queue
                    workers.add(w);
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

What really runs the thread is the runWorker method

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        //Call threads in workQueue iteratively, then call their run methods
        while (task != null || (task = getTask()) != null) {
            w.lock();
            // If pool is stopping, ensure thread is interrupted;
            // if not, ensure thread is not interrupted.  This
            // requires a recheck in second case to deal with
            // shutdownNow race while clearing interrupt
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                try {
                    task.run();
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

The state of the thread pool

Thread pools are stateful just like threads. Here is a simple list of the states of a thread pool

The state of the thread poolMeaning
RUNNINGAccept new tasks and process queued tasks
SHUTDOWNDo not accept new tasks, but handle queued tasks
STOPDo not accept new tasks, do not process queued tasks, and interrupt running tasks
TIDYINGAll tasks have been terminated, workerCount is 0, start running terminate() method
TERMINATEDThread pool completely stopped

summary

A simple comb of the contents of the thread pool means that tasks are reused through several active threads, but when using the thread pool, you need to avoid too many threads to create and too many tasks to pile up. The hook functions for thread pools are not summarized here, so come back after you have combed the concept of locks in concurrent programming.

Tags: Java Back-end

Posted on Wed, 10 Nov 2021 14:13:31 -0500 by Chesso