A brief introduction to thread pools in Java programming

  • 2020-04-01 04:08:16
  • OfStack

Starting with Java 5, Java provides its own thread pool. A thread pool is a container of threads that execute a nominal number of threads at a time. Java. Util. Concurrent. ThreadPoolExecutor is such a thread pool. It's flexible, but it's also complex to use, and this article is about it.

The first is the constructor. Take the simplest constructor as an example:


public ThreadPoolExecutor( 
      int corePoolSize, 
      int maximumPoolSize, 
      long keepAliveTime, 
      TimeUnit unit, 
      BlockingQueue<Runnable> workQueue) 

It looks complicated. Here's an introduction.
CorePoolSize refers to the reserved thread pool size.
MaximumPoolSize refers to the maximum size of the thread pool.
KeepAliveTime refers to the timeout at the end of a free thread.
Unit is an enumeration that represents the unit of keepAliveTime.
A workQueue is a queue that holds tasks.
We can learn the meaning of these parameters from the way the thread pool works. The thread pool works as follows:

1. When the thread pool was first created, there was no thread in it. The task queue is passed in as an argument. However, even if there are tasks in the queue, the thread pool does not execute them immediately.
 
2. When the execute() method is called to add a task, the thread pool makes the following judgment:
      A. If the number of threads running is less than corePoolSize, create a thread to run the task immediately;
      B. If the number of threads running is greater than or equal to corePoolSize, put the task on the queue.
      C. If the queue is full and the number of threads running is less than maximumPoolSize, create a thread to run the task;
      D. If the queue is full and the number of threads running is greater than or equal to maximumPoolSize, the thread pool throws an exception, telling the caller, "I can no longer accept tasks."
 
3. When a thread completes a task, it takes the next task from the queue to execute.
 
4. When a thread has nothing to do for more than a certain amount of time, the thread pool determines that if the number of threads currently running is greater than corePoolSize, the thread is stopped. So when all the tasks for the thread pool are done, it will eventually shrink to the size of corePoolSize.
 
This process shows that the task is not added first and will be executed first. Assuming a queue size of 10, corePoolSize of 3, and maximumPoolSize of 6, when 20 tasks are added, the order of execution is this: tasks 1, 2, 3 are executed first, and then tasks 4-13 are queued. When the queue is full, tasks 14, 15, and 16 are executed immediately, and tasks 17 to 20 throw an exception. The final order is: 1, 2, 3, 14, 15, 16, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13. Here's an example of using a thread pool:


public static void main(String[] args) { 
  BlockingQueue<Runnable> queue = new LinkedBlockingQueue<Runnable>(); 
  ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 1, TimeUnit.DAYS, queue); 
  
  for (int i = 0; i < 20; i++) { 
    executor.execute(new Runnable() { 
  
      public void run() { 
        try { 
          Thread.sleep(1000); 
        } catch (InterruptedException e) { 
          e.printStackTrace(); 
        } 
        System.out.println(String.format("thread %d finished", this.hashCode())); 
      } 
    }); 
  } 
  executor.shutdown(); 
} 

The illustration of this example is as follows:
 
1. BlockingQueue is just an interface. Common implementation classes are LinkedBlockingQueue and ArrayBlockingQueue. The advantage of using LinkedBlockingQueue is that there is no size limit. Then, because the queue will not be full, execute() will not throw an exception, and the number of threads running in the thread pool will never exceed corePoolSize, and the keepAliveTime parameter will be meaningless.
 
2. The shutdown() method does not block. After the shutdown() method is called, the main thread ends immediately, and the thread pool continues to run until all the tasks are completed. If you do not call the shutdown() method, the thread pool is kept open for new tasks to be added at any time.
 
There is only a small introduction to this thread pool. ThreadPoolExecutor is highly extensible, but extending it requires familiarity with how it works. A later article will show you how to extend the ThreadPoolExecutor class.

Ava. Util. Concurrent. ThreadPoolExecutor class provides a rich extensibility. You can customize its behavior by subclassing it. For example, if I want to print a message after each task, but I cannot modify the task object, I can write:


ThreadPoolExecutor executor = new ThreadPoolExecutor(size, maxSize, 1, TimeUnit.DAYS, queue) {
  @Override
  protected void afterExecute(Runnable r, Throwable t) {
    System.out.println("Task finished.");
  }
};


In addition to afterExecute methods, the ThreadPoolExecutor class also has beforeExecute() and terminate () methods that can be overridden, before the task executes and after the entire thread pool stops, respectively.


In addition to adding actions before and after a task is executed, ThreadPoolExecutor also allows you to customize the execution strategy when adding a task fails. You can call the thread pool setRejectedExecutionHandler () method, using custom RejectedExecutionHandler objects replace the existing strategy. ThreadPoolExecutor provides four existing policies, which are:
ThreadPoolExecutor. AbortPolicy: rejected tasks and throw an exception
ThreadPoolExecutor. DiscardPolicy: rejected tasks but do not do any action
ThreadPoolExecutor. CallerRunsPolicy: refused to task, and directly in the thread of the caller perform this task
ThreadPoolExecutor. DiscardOldestPolicy: said to discard the first task in the task queue, and then add the task in the queue.
Here's an example:
ThreadPoolExecutor = new ThreadPoolExecutor(size, maxSize, 1, TimeUnit.DAYS, queue);
Executor. SetRejectedExecutionHandler (new ThreadPoolExecutor. DiscardPolicy ());

In addition, you can write your own policies by implementing the RejectedExecutionHandler interface. Here's an example:


ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 1, TimeUnit.SECONDS, queue,
    new RejectedExecutionHandler() {
      public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        System.out.println(String.format("Task %d rejected.", r.hashCode()));
      }
    }
);


Related articles: