The foreword 0.

Thread pools are the most widely used concurrency framework in Java, and there are many benefits to using thread pools properly:

  • Reduced resource consumption: Reusing created threads reduces resource consumption when threads are created and destroyed
  • Improved response time: Tasks can be executed immediately when they arrive without waiting for a thread to be created
  • Improved thread manageability: The infinite creation of threads not only consumes system resources, but also reduces system stability, and can be uniformly allocated, tuned, and monitored through thread pools

1. Implementation principle of thread pool

1.1 Thread pools handle tasks

When submitting a task to a thread pool, the main process is:

  • Step1: Thread pool Determines whether the core thread pool is full. If it is not full, a new thread is created to execute the task; if it is full, the next process is moved on
  • Step2: the thread pool determines whether the task queue is full. If it is not full, the task is added to the task queue. If it is full, the task goes to the next process
  • Step3: thread pool determine whether the thread pool is full. If it is not full, a new thread is created to execute the task; if it is full, it is handed over to the rejection policy

1.2 Executing the execute method

When ThreadPoolExecutor executes execute(), the process is as follows:

  • Step1: If the number of running threads is less than corePoolSize, create a new thread to execute the task (global lock required)
  • Step2: If the number of running threads is greater than or equal to corePoolSize, add the task to BlockingQueue
  • Step3: if the task cannot be added to the BlockingQueue (queue is full), create a new thread to execute the task (global lock is required)
  • Step4: if creating a new thread causes the number of currently running threads to exceed maximumPoolSize, the task will be rejected and the RejectedExecutionHandler’srejectedExecution()methods

ThreadPoolExecutor uses the above steps to process tasks in order to avoid acquiring global locks as much as possible (which would be a serious scalable bottleneck)

1.3 Worker Worker thread

When creating a thread in the thread pool, it will encapsulate the thread as a Worker thread. After the Worker thread executes the initial task, it will cycle to obtain the task execution from the task queue

There are two cases when a worker thread executes a task:

  • inexecute()Method creates a thread that executes the current task
  • After the thread completes the initial task in Step 1, it loops to retrieve the task execution from BlockingQueue

2. Use of thread pools

2.1 Creating a thread pool

ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
Copy the code
  • CorePoolSize: number of core threads

  • MaximumPoolSize: maximum number of threads. This parameter is invalid if the thread pool uses an unbounded task queue

  • KeepAliveTime: indicates the keepAliveTime of a thread when the worker thread is idle (no work can be obtained from the task queue)

  • Unit: Unit of the thread lifetime

  • WorkQueue: Task queue, which stores the blocking queue of tasks waiting to be executed

    • ArrayBlockingQueue: A bounded blocking queue based on an array structure that sorts elements in FIFO

    • LinkedBlockingQueue: An unbounded blocking queue based on a linked list that sorts elements in FIFO. Throughput is higher than ArrayBlockingQueue queue, Executors. NewFixedThreadPool () method USES the queue

    • SynchronousQueue: A queue that does not store elements and blocks each insert until another thread removes it. Throughput is higher than LinkedBlockingQueue queue, Executors. NewSingleThreadExecutor () method USES the queue

    • PriorityBlockingQueue: An unbounded blocking queue with a priority

  • ThreadFactory: Factory for creating threads

  • Handler: Reject policy that requires a policy to process submitted tasks when both worker threads and task queues are full

    • AbortPolicy: Directly throws an exception
    • CallerRunsPolicy: Only the caller’s thread executes the task
    • DiscardOldestPolicy: Discards the last task in the task queue and then invokes itexecute()Method processing task
    • DiscardPolicy: Discards tasks without processing them

2.2 Submitting tasks to a thread pool

  • execute()Method: Submit a task that does not require a return value
  • submit()Methods: To submit a task that requires a return value, you can obtain the return value by using the following methods
    • Future.get()Method: block the caller’s thread until the result is returned
    • Future.get(long timeout, TimeUnit unit)Method: block the caller’s thread and return immediately after the specified time. The task may not be completed

2.3 Disabling a thread pool

  • shutdown()Method: Try to interrupt idle threads by setting the state of the thread pool to SHUTDOWN
  • shutdownNow()Method: Set the state of the thread pool to STOP and try to STOP all threads (executing, idle)

2.4 Configure thread pools properly

Criteria for configuring thread pools:

  • Nature of the task:
    • CPU intensive: as few threads as possible, such as cpuNum + 1 thread
    • IO intensive: as many threads as possible, such as cpuNum * 2 threads
    • Hybrid: Split the task into one CPU intensive task and one IO intensive task if it can be split. If the execution time difference between two tasks is small, the throughput of decomposed execution will be higher than serial execution. If the time difference between the two tasks is large, there is no need to split them
  • Task priority: high, medium, low
    • Tasks with different priorities can be processed using priority task queues (there may be tasks with lower priorities that cannot be executed)
  • Task execution time: long, medium, short
    • Tasks with different execution times can be assigned to thread pools of different sizes, or priority task queues can be used to allow shorter tasks to be executed first
  • Task dependencies: Whether they depend on other system resources, such as database connections
    • Tasks that depend on the database connection pool, because threads submit SQL and wait for the database to return results, the longer the wait the longer the CPU is idle, the more threads can be set up to make full use of CPU resources

It is suggested to use bounded queue to increase the stability and warning ability of the system, and avoid the infinite accumulation of tasks caused by thread blocking, and finally the system may be unavailable due to lack of memory (OOM)

2.5 Thread pool monitoring

If thread pools are heavily used in your system, it is necessary to monitor them. Thread pool monitoring methods:

  • getTaskCount()Method: Obtain the number of tasks to be executed
  • getCompletedTaskCount()Method: Obtain the number of completed tasks that have been executed
  • getLargestPoolSize()Method: Get the maximum number of threads ever created
  • getPoolSize()Method: Get the number of worker threads
  • getActiveCount()Method: Get the number of threads active (executing a task)

You can also customize the thread pool by inheriting the thread pool, overriding beforeExecute(), afterExecute(), terminated() methods, and executing code to monitor before and after task execution but before the thread pool is closed

From The Art of Concurrent Programming in Java