Java multithreading: understand thread pool thoroughly

Keywords: Java less Database

Advantages of thread pool

In general, thread pool has the following advantages:

(1) Reduce resource consumption. Reduce the cost of thread creation and destruction by reusing the created threads.

(2) Improve response speed. When a task arrives, it can be executed without waiting for the thread to be created.

(3) Improve the manageability of threads. Threads are scarce resources. If unlimited creation, it will not only consume system resources, but also reduce the stability of the system. Using thread pool can make unified allocation, tuning and monitoring.

Use of thread pool

The real implementation class of thread pool is ThreadPoolExecutor, which can be constructed in the following four ways:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         threadFactory, defaultHandler);
}

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          RejectedExecutionHandler handler) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), handler);
}

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.corePoolSize = corePoolSize;
    this.maximumPoolSize = maximumPoolSize;
    this.workQueue = workQueue;
    this.keepAliveTime = unit.toNanos(keepAliveTime);
    this.threadFactory = threadFactory;
    this.handler = handler;
}

As you can see, it needs the following parameters:

  • corePoolSize (required): number of core threads. By default, the core thread will live all the time, but when allowCoreThreadTimeout is set to true, the core thread will also timeout recycling.
  • Maximum poolsize (required): the maximum number of threads that the thread pool can hold. When the number of active threads reaches this value, subsequent new tasks will block.
  • keepAliveTime (required): thread idle timeout. If the duration is exceeded, non core threads are recycled. If allowCoreThreadTimeout is set to true, the core thread will also time out to recycle.
  • Unit (required): Specifies the time unit of the keepAliveTime parameter. Commonly used ones are: TimeUnit.MILLISECONDS, TimeUnit.SECONDS and TimeUnit.MINUTES.
  • workQueue (required): task queue. The Runnable object submitted through the execute() method of the thread pool is stored in this parameter. It is implemented by blocking queue.
  • threadFactory (optional): thread factory. Specifies how new threads are created for the thread pool.
  • handler (optional): reject policy. The saturation policy that needs to be executed when the maximum number of threads is reached.

The usage process of thread pool is as follows:

// Create thread pool
Executor threadPool = new ThreadPoolExecutor(CORE_POOL_SIZE,
                                             MAXIMUM_POOL_SIZE,
                                             KEEP_ALIVE,
                                             TimeUnit.SECONDS,
                                             sPoolWorkQueue,
                                             sThreadFactory);
// Submit tasks to thread pool
threadPool.execute(new Runnable() {
    @Override
    public void run() {
        ... // Tasks performed by threads
    }
});
// Close thread pool
threadPool.shutdown(); // Set the status of thread pool to SHUTDOWN, and then interrupt all threads without executing tasks
threadPool.shutdownNow(); // Set the status of thread pool to STOP, and then try to STOP all threads executing or suspending tasks, and return the list of waiting tasks

How thread pools work

Let's describe the working principle of a downline pool and have a deeper understanding of the above parameters. Its working principle flow chart is as follows:

[failed to transfer the pictures in the external link. The source station may have anti-theft chain mechanism. It is recommended to save the pictures and upload them directly (img-vtugosb-1583835377350) (assets \ 20190809200646357. PNG))

[failed to transfer the pictures in the external link. The source station may have anti-theft chain mechanism. It is recommended to save the pictures and upload them directly (img-FMFPHgea-1583835377351)(assets94380-b98a1a0bbef64e0d.png))

If the current thread number is less than corepoolsize, create a new thread to perform the task

If the current number of threads is > = corepoolsize, the task will be stored in BlockingQueue

If the blocking queue is full and the current number of threads is < maximumpoolsize, the new thread performs the task.

If the blocking queue is full and the current number of threads is > = maximumpoolsize, an exception RejectedExecutionException is thrown to tell the caller that the task can no longer be accepted.

Parameters for thread pool

Task queue

Task queue is based on blocking queue, that is to say, using producer consumer mode, we need to implement BlockingQueue interface in Java. But Java has provided us with 7 kinds of implementation of blocking queues:

  1. ArrayBlockingQueue: a bounded blocking queue composed of an array structure (the array structure can implement a circular queue with a pointer).
  2. LinkedBlockingQueue: a bounded blocking queue composed of linked list structure. When the capacity is not specified, the capacity defaults to integer.max'value.
  3. PriorityBlockingQueue: an unbounded blocking queue that supports priority sorting. There is no requirement for elements. You can implement the Comparable interface or provide a Comparator to compare elements in the queue. It has nothing to do with time. It just takes tasks according to priority.
  4. DelayQueue: similar to PriorityBlockingQueue, it is an unbounded priority blocking queue implemented by binary heap. It is required that all elements implement the Delayed interface to extract tasks from the queue through the execution delay, and the tasks cannot be retrieved before the time is up.
  5. SynchronousQueue: A blocking queue without storing elements will block when the consumer thread calls the take() method. Until a producer thread produces an element, the consumer thread can get the element and return it. When the producer thread calls the put() method, it will also block until a consumer thread consumes an element, the producer will return it.
  6. LinkedBlockingDeque: a bounded two terminal blocking queue implemented with two-way queues. Dual end means that you can FIFO (first in, first out) like a normal queue or FILO (first in, then out) like a stack.
  7. LinkedTransferQueue: it is a combination of ConcurrentLinkedQueue, LinkedBlockingQueue and SynchronousQueue, but it is used in ThreadPoolExecutor, consistent with LinkedBlockingQueue behavior, but it is an unbounded blocking queue.

Note the difference between bounded queue and unbounded queue: if bounded queue is used, the rejection policy will be executed when the queue is saturated and the maximum number of threads is exceeded; but if unbounded queue is used, because task queue can always add tasks, there is no point in setting maximumPoolSize.

Thread factory

The thread factory specifies the way to create a thread. It needs to implement the ThreadFactory interface and the **newThread(Runnable r) * * method. This parameter can be unspecified. The Executors framework has implemented a default thread factory for us:

/**
 * The default thread factory.
 */
private 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;
    }
}

Deny policy (handler)

When the number of threads in the thread pool reaches the maximum number of threads, a denial policy is required. The rejection policy needs to implement the RejectedExecutionHandler interface and the rejectedExecution(Runnable r, ThreadPoolExecutor executor) method. However, the Executors framework has implemented four rejection strategies for us:

  1. AbortPolicy (default): discards the task and throws a RejectedExecutionException exception.
  2. CallerRunsPolicy: the task is processed by the calling thread.
  3. DiscardPolicy: discards the task without throwing an exception. This mode can be used for customized processing.
  4. DiscardOldestPolicy: discard the oldest unprocessed task in the queue, and then try to execute the task again.

Functional thread pool

In fact, Executors have encapsulated four common function thread pools for us, as follows:

  • Fixed thread pool
  • Scheduled thread pool
  • CachedThreadPool
  • Single threaded executor

Fixed thread pool

Source code of creation method:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}
  • Features: only core threads, fixed number of threads, recycle immediately after execution, and task queue is a bounded queue with linked list structure.
  • Application scenario: control the maximum concurrent number of threads.

Use example:

// 1. Create a fixed length thread pool object & set the thread pool thread number to 3
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 2. Create a Runnable class thread object & tasks to be executed
Runnable task =new Runnable(){
  public void run() {
     System.out.println("I'm on duty");
  }
};
// 3. Submit tasks to thread pool
fixedThreadPool.execute(task);

Scheduled thread pool

Source code of creation method:

private static final long DEFAULT_KEEPALIVE_MILLIS = 10L;
 
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
 
public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
                                   ThreadFactory threadFactory) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue(), threadFactory);
}
  • Features: the number of core threads is fixed, the number of non core threads is unlimited, and the task queue is a delay blocking queue.
  • Application scenario: perform scheduled or periodic tasks.

Use example:

// 1. Create a timed thread pool object & set the number of thread pool threads to 5
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
// 2. Create a Runnable class thread object & tasks to be executed
Runnable task =new Runnable(){
  public void run() {
     System.out.println("I'm on duty");
  }
};
// 3. Submit tasks to thread pool
scheduledThreadPool.schedule(task, 1, TimeUnit.SECONDS); // Perform task after 1 s delay
scheduledThreadPool.scheduleAtFixedRate(task,10,1000,TimeUnit.MILLISECONDS);// Perform tasks every 1000ms after 10ms delay

CachedThreadPool

Source code of creation method:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}
  • Features: no core threads, unlimited number of non core threads, recycling after 60 seconds of idle execution, the task queue is a blocking queue without storage elements.
  • Application scenario: perform a large number of tasks with less time consumption.

Use example:

// 1. Create a cacheable thread pool object
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. Create a Runnable class thread object & tasks to be performed
Runnable task =new Runnable(){
  public void run() {
     System.out.println("I'm on duty");
  }
};
// 3. Submit tasks to thread pool
cachedThreadPool.execute(task);

Single threaded executor

Source code of creation method:

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

  • Features: there is only one core thread, no non core thread, and the task queue is a bounded queue with linked list structure.
  • Application scenario: operations that are not suitable for concurrency but may cause IO blocking and affect UI thread response, such as database operations, file operations, etc.

Use example:

// 1. Create a single threaded pool
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 2. Create a Runnable class thread object & tasks to be executed
Runnable task =new Runnable(){
  public void run() {
     System.out.println("I'm on duty");
  }
};
// 3. Submit tasks to thread pool
singleThreadExecutor.execute(task);

Contrast

[failed to transfer the pictures in the external link. The source station may have anti-theft chain mechanism. It is recommended to save the pictures and upload them directly (img-IIU9bLFT-1583835377351)(assets190721095954211.png))

Common thread pool executor example

Now there is a taskworktask

public class WorkTask implements Runnable{
	public void run() {
		try {
			int r = (int)(Math.random()*10);
			Thread.sleep(r*1000);
			System.out.println(Thread.currentThread().getId() + " is over");
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}

CachedThreadPool

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class CachedThreadPoolTest {
	public static void main(String[] args) {
		ExecutorService exec = Executors.newCachedThreadPool();
		for(int i=0;i<20;i++){
			exec.execute(new WorkTask());
		}
		exec.shutdown();
	}
}

Unlimited thread pool. You can create up to integer.max'value threads. There is no duplicate thread number in the running result

FixedThreadPool

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class FixedThreadPoolTest {
	public static void main(String[] args) {
		ExecutorService exec = Executors.newFixedThreadPool(3);
		for(int i=0;i<20;i++){
			exec.execute(new WorkTask());
		}
		exec.shutdown();
		
	}
}

Three threads perform 20 tasks

SingleThreadExecutor

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class SingleThreadExecutorTest {
	public static void main(String[] args) {
		ExecutorService exec = Executors.newSingleThreadExecutor();
		for(int i=0;i<20;i++){
			exec.execute(new WorkTask());
		}
		exec.shutdown();
	}
}

Always 1 thread to perform all tasks

Customize your own non blocking thread pool

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;


public class CustomThreadPoolExecutor {

    
    private ThreadPoolExecutor pool = null;
    
    
    /**
     * Thread pool initialization method
     * 
     * corePoolSize Core thread pool size - 10
     * maximumPoolSize Maximum thread pool size ----- 30
     * keepAliveTime The maximum survival time of idle threads in the thread pool exceeding the number of corePoolSize ----- 30 + unit TimeUnit
     * TimeUnit keepAliveTime Time unit --- TimeUnit.MINUTES
     * workQueue Blocking queue - New arrayblockingqueue < runnable > (10) ===== 10 capacity blocking queue
     * threadFactory New thread factory - new CustomThreadFactory() ===== customized thread factory
     * rejectedExecutionHandler When the number of submitted tasks exceeds the sum of maxmumPoolSize+workQueue,
     *                             That is, when the 41st task is submitted (the previous threads have not finished executing, sleep (100) is used in this test method),
     *                                   The task is handed over to the RejectedExecutionHandler for processing
     */
    public void init() {
        pool = new ThreadPoolExecutor(
                10,
                30,
                30,
                TimeUnit.MINUTES,
                new ArrayBlockingQueue<Runnable>(10),
                new CustomThreadFactory(),
                new CustomRejectedExecutionHandler());
    }

    
    public void destory() {
        if(pool != null) {
            pool.shutdownNow();
        }
    }
    
    
    public ExecutorService getCustomThreadPoolExecutor() {
        return this.pool;
    }
    
    private class CustomThreadFactory implements ThreadFactory {

        private AtomicInteger count = new AtomicInteger(0);
        
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            String threadName = CustomThreadPoolExecutor.class.getSimpleName() + count.addAndGet(1);
            System.out.println(threadName);
            t.setName(threadName);
            return t;
        }
    }
    
    
    private class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            // Record exception
            // Alarm processing, etc
            System.out.println("error.............");
        }
    }
    
    
    
    // Test constructed thread pool
    public static void main(String[] args) {
        CustomThreadPoolExecutor exec = new CustomThreadPoolExecutor();
        // 1. initialization
        exec.init();
        
        ExecutorService pool = exec.getCustomThreadPoolExecutor();
        for(int i=1; i<100; i++) {
            System.out.println("Submission" + i + "Task!");
            pool.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("running=====");
                }
            });
        }
        
        
        
        // 2. Destroy: you cannot destroy it here because the task has not been submitted for execution. If you destroy the thread pool, the task cannot be executed
        // exec.destory();
        
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

summary

Although the four function thread pools of Executors are convenient, they are not recommended to be used now. Instead, it is recommended to use the ThreadPoolExecutor directly. Such a processing method enables the students who write to be more clear about the running rules of the thread pool and avoid the risk of resource exhaustion.

In fact, the four functional threads of Executors have the following disadvantages:

  1. FixedThreadPool and singlethreadexecution: the main problem is that the stacked request processing queue uses LinkedBlockingQueue, which may consume a lot of memory, even OOM.
  2. CachedThreadPool and ScheduledThreadPool: the main problem is that the maximum number of threads is integer.max'value, which may create a very large number of threads, or even OOM.
Published 24 original articles, won praise 1, visited 436
Private letter follow

Posted by blackandwhite on Tue, 10 Mar 2020 04:08:25 -0700