Java thread pool details

The previous article introduces the thread related content in detail, but in the usual development work, we rarely create a thread directly for use, which is generally called through the thread pool. This article will introduce how thread pools work in Java and the differences between various thread pools

1, Threads and thread pools

We can see the difference between threads and thread pools by executing the same code

Create multiple threads:

Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 100000; i++) {
    Thread thread = new Thread() {
        @Override
        public void run() {
            list.add(random.nextInt());

        }
    };
    thread.start();
}
System.out.println("Time:" + (System.currentTimeMillis() - start));

Time: 14729

Thread pool:

Long start = System.currentTimeMillis();
final Random random = new Random();
final List<Integer> list = new ArrayList<Integer>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 100000; i++) {
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            list.add(random.nextInt());
        }
    });
}
executorService.shutdown();
System.out.println("Time:"+(System.currentTimeMillis() - start));
Time: 21

Through the above two methods, we can clearly see that there is an obvious performance difference between creating multiple threads and using thread pool. The essential reason for this situation is that threads are a heavy resource for the operating system, and their creation and destruction require a lot of extra CPU time. The thread pool can avoid the consumption of creating a large number of threads through thread reuse, so as to achieve the purpose of high performance

2, Thread pool creation

The Executors class provides these four ways to create threads, but we do not recommend them. Because the relevant parameters of the thread pool created by this way are all default, which is easy to cause program OOM. Therefore, we prefer to use the new key to create the thread pool and manually specify the configuration parameters of the thread pool

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);

We can take a look at the source code of the above four methods until we know how to create a thread pool

  • newSingleThreadExecutor

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
  • newCachedThreadPool

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    
  • newFixedThreadPool

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    
  • newScheduledThreadPool

    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }
    
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
    

It can be seen from the source code of the above methods that they are internally to create a ThreadPoolExecutor instance, but the corresponding parameters of each method are different. It is recommended to directly use this method to create a thread pool.

3, Thread pool core parameters

All methods of creating thread pools will finally call the following construction method, which we will introduce according to the input parameters of this method

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    if (corePoolSize < 0 ||
        maximumPoolSize <= 0 ||
        maximumPoolSize < corePoolSize ||
        keepAliveTime < 0)
        throw new IllegalArgumentException();
    if (workQueue == null || threadFactory == null || handler == null)
        throw new NullPointerException();
    this.acc = System.getSecurityManager() == null ?
        null :
    AccessController.getContext();
    this.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

3.1 corePoolSize

Number of core threads (number of resident threads). When a task is added to the thread pool, a new thread will be created to execute the task as long as the number of threads in the current thread pool does not reach the number of core threads. After all tasks are executed, if the number of threads in the thread pool is greater than the number of core threads, the extra threads will be destroyed and only the number of core threads will be reserved for subsequent use.

3.2 maximumPoolSize

As the name suggests, it is the maximum number of threads allowed to exist simultaneously in the thread pool. When the number of tasks is greater than the number of core threads, new tasks will be added to the queue for waiting. When the queue is also full, it will judge whether the number of threads in the current thread pool is greater than the maximum number of threads allowed in the thread pool. If it is less than the maximum number of threads, New threads are created for these tasks, and these new threads are temporary threads.

When creating a thread pool through Executors, this value is usually set to integer MAX_ Value, when there are too many tasks, it will lead to OOM

3.3 keepAliveTime&TimeUnit

This parameter specifies the survival time of the temporary thread, and the TimeUnit parameter specifies the unit of time to convert the transmitted time into nanoseconds. The maximumPoolSize parameter indicates that temporary threads are created when the task queue is full. The reason why they are temporary threads is that these temporary threads will be destroyed after all the tasks in the thread pool are executed. When to destroy them is determined by the keepAliveTime parameter. After this time, they will be destroyed

At the same time, this parameter can also be used for core threads. If the allowCoreThreadTimeOut parameter is set to true in the thread pool, it means that if the waiting time of core threads exceeds keepAliveTime, it will also be recycled

3.4 BlockingQueue

Blocking queue: when adding a task to the thread pool, if the number of threads in the thread pool has reached the number of core threads, the newly added task will be cached in the blocking queue first. When the task of a thread is completed, the task will be obtained from the blocking queue to continue execution.

The four methods of creating thread pools in the Executors class use LinkedBlockingQueue, SynchronousQueue and DelayedWorkQueue respectively

SynchronousQueue is a synchronous queue that does not store tasks. When a thread put s a task, the thread will be blocked. The blocked thread will not wake up until a thread calls the take() method. It implements the one-to-one message transmission model between threads, and newCachedThreadPool() uses this queue, which will lead to a phenomenon, When the number of threads reaches the number of core threads, when a new task comes in, it will be blocked. You need to create a new thread to receive the task just now, otherwise you can't add another task.

LinkedBlockingQueue is a linked list blocking queue, but it is a bounded queue. When we create a thread pool through the newsingthreadexecution() method, we directly create a blocking queue by new LinkedBlockingQueue < runnable > (), but the default capacity of this blocking queue is integer MAX_ Value, which is also one of the reasons for OOM in the program, so we must specify its capacity when creating a blocking queue.

public LinkedBlockingQueue() {
    this(Integer.MAX_VALUE);
}

DelayedWorkQueue is a delay queue, which is mainly used in the timed thread pool. It provides a default initialization capacity of 16 and can be expanded

private static final int INITIAL_CAPACITY = 16;

private void grow() {
    int oldCapacity = queue.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
    if (newCapacity < 0) // overflow
        newCapacity = Integer.MAX_VALUE;
    queue = Arrays.copyOf(queue, newCapacity);
}

3.5 ThreadFactory

In the thread factory and thread pool, thread objects are created through the newThread() method of ThreadFactory. Among the four methods of Executors, DefaultThreadFactory is used as the thread factory

3.6 RejectedExecutionHandler

Rejection strategy, that is, when the number of tasks fills the blocking queue and the current number of threads has reached the maximum number of threads specified by the thread pool, a new task will come in, and the corresponding rejection strategy will be adopted to process the new task.

ThreadPoolExecutor provides four rejection policies:

CallerRunsPolicy: the thread that calls the execute() method of the thread pool executes the current task

AbortPolicy: throw RejectedExecutionException directly

DiscardPolicy: ignore the task and do nothing

Discard oldest policy: remove the longest pending task in the task queue and resubmit it

The ThreadPoolExecutor uses AbortPolicy as the default rejection policy, but in actual development, it will generally implement the RejectedExecutionHandler interface to customize the rejection policy

3.7 other core parameters

In addition to the above parameters specified when creating a thread pool, the thread pool also has some other core parameters, such as recording the thread pool status and the number of threads

In ThreadPoolExecutor, the ctl attribute of INT type is used to record the thread pool status and the number of threads at the same time. The upper three bits are used to record the thread pool status, and the lower 29 bits are used to record the number of threads in the thread pool

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// runState is stored in the high-order bits
private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

The transition diagram of thread pool state is as follows:

The differences between the shutdown() and shutdown now () methods are as follows:

After the thread pool executes the shutdown() method, you can no longer add tasks to the thread pool, but the added tasks will continue to be executed; After the shutdown now () method is executed, not only cannot tasks be added to the thread pool, but also the added tasks will not be executed. All threads will be interrupted inside the method

Keywords: Java Back-end thread pool

Added by ekosoftco on Wed, 05 Jan 2022 22:14:40 +0200