In-depth analysis of Java multithreading makes our high-concurrency programs more robust

1. The concept and function of thread pool

What is a thread pool?

A thread pool is a mechanism for managing and reusing thread resources. It can create a group of pre-initialized threads in an application to perform multiple tasks. The thread pool maintains a thread queue, which contains a certain number of idle threads. When a new task arrives, the idle thread in the thread pool will execute the task immediately without creating and destroying the thread every time, thus improving the performance and efficiency of the application.

The main purpose of using the thread pool is to avoid the overhead caused by frequent creation and destruction of threads, and at the same time effectively control the number of concurrent threads to prevent the waste of system resources and the overhead of thread switching caused by too many threads.

Advantages of thread pool

  • Improve system performance: The thread pool can avoid frequent creation and destruction of threads, reducing the overhead of thread creation and context switching.
  • Improve response speed: Idle threads in the thread pool can execute new tasks immediately, reducing the waiting time for tasks.
  • Control concurrent resources: By adjusting the parameters of the thread pool, the number of concurrent threads can be limited to prevent excessive occupation of system resources.
  • Provide management and monitoring mechanism: The thread pool provides unified management and monitoring of threads, which can easily manage thread status, execution status and exception handling.

The principle and working method of thread pool

A thread pool usually consists of the following components:

  1. Thread Pool Manager (ThreadPoolExecutor): Responsible for the creation, destruction and management of the entire thread pool, and dynamically adjust the number of threads according to the submission of tasks.
  2. Worker Threads: Threads that actually execute tasks. They are managed by the thread pool, can be reused, and enter an idle state to wait for new tasks after the task is executed.
  3. Task Queue (Task Queue): used to store unexecuted tasks, the thread pool will obtain tasks from the task queue, and then hand them over to idle threads for execution.
  4. Thread Pool Parameters (Thread Pool Parameters): including the number of core threads, maximum number of threads, thread survival time and other parameters, used to control the behavior and performance of the thread pool.

The working process of the thread pool is as follows:

  1. Initializes the thread pool, creates the specified number of worker threads, and adds them to the thread pool.
  2. When a task is submitted to the thread pool, the thread pool will obtain a task from the task queue and assign the task to an idle worker thread for execution.
  3. If there are currently no idle threads and the number of worker threads has not reached the maximum number of threads, a new thread will be created to perform the task.
  4. If the task queue is full and the number of worker threads has reached the maximum number of threads, the unexecutable tasks are processed according to the preset rejection policy.
  5. After the worker thread finishes executing the task, it returns to the thread pool and waits for new tasks to arrive.
  6. When the thread pool no longer needs to perform tasks, the thread pool can be closed to release resources.

Thread pool is an important concept in multi-threaded programming. By effectively reusing threads and reasonably managing the number of concurrent threads, program performance, response speed, and resource utilization can be improved.

2. Create a thread pool

Create a thread pool using the Executors class

The Executors class is a tool class provided in the Java standard library for creating and managing thread pools. It provides some static methods to easily create different types of thread pools.

  1. Create a fixed-size thread pool (FixedThreadPool): The thread pool will create a fixed number of threads to perform tasks. If the threads in the thread pool are busy, new tasks will enter the task queue for execution.
ExecutorService executor = Executors.newFixedThreadPool(int nThreads);

Among them, nThreads specifies the number of threads in the thread pool.

  1. Create a single-threaded thread pool (SingleThreadExecutor): This thread pool will only create one thread to execute tasks, ensuring that all tasks are executed in the specified order (FIFO).
ExecutorService executor = Executors.newSingleThreadExecutor();
  1. Create a cacheable thread pool (CachedThreadPool): The thread pool creates new threads to perform tasks as needed, and automatically reclaims thread resources after the thread is idle for a period of time.
ExecutorService executor = Executors.newCachedThreadPool();
  1. Create a thread pool for scheduled tasks (ScheduledThreadPool): This thread pool can be used to execute scheduled tasks and periodic tasks. You can set delayed execution tasks, scheduled execution tasks, and execute tasks at a fixed frequency.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(int corePoolSize);

Among them, corePoolSize specifies the number of core threads.

The thread pools created by the above methods all implement the ExecutorService interface, and you can use the methods provided by this interface to submit tasks, close thread pools, and other operations.

For example, you can use the execute(Runnable command) method to submit a task to the thread pool for execution:

executor.execute(new Runnable() {
    
    
    public void run() {
    
    
        // 任务的具体逻辑
    }
});

You can also close the thread pool by calling the shutdown() method so that it will no longer accept new tasks, and will wait for the submitted tasks to complete before closing:

executor.shutdown();

The Executors class provides a convenient method for creating a thread pool, which simplifies the creation process of the thread pool, and also provides flexible configuration options to facilitate the selection of the appropriate thread pool type according to the application scenario. However, it should be noted that for long-running applications, it is recommended to manually create a thread pool and perform fine configuration to meet the performance and resource management requirements of the application.

Parameter setting and creation process of custom thread pool

A custom thread pool can be implemented through the ThreadPoolExecutor class, which provides more flexible parameter setting and creation process.

  1. Create a ThreadPoolExecutor object:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue
);
  • corePoolSize: Specifies the number of core threads in the thread pool, that is, the minimum number of threads in the thread pool.
  • maximumPoolSize: Specifies the maximum number of threads in the thread pool, that is, the maximum number of threads allowed to be created in the thread pool.
  • keepAliveTime: Specifies the survival time of idle threads exceeding the number of core threads, that is, when the number of threads in the thread pool exceeds corePoolSize, the redundant idle threads will be destroyed if they are not used within a certain period of time.
  • unit: Specify the time unit of keepAliveTime, you can choose TimeUnit.SECONDS, TimeUnit.MILLISECONDS, etc.
  • workQueue: Specifies the task queue for storing tasks that have not yet been executed.
  1. Set a rejection policy (optional):
    If the task queue of the thread pool is full and cannot accept new tasks, you can set a rejection policy to handle the tasks that cannot be executed. ThreadPoolExecutor provides four predefined rejection strategies:
  • AbortPolicy (default): Throw RejectedExecutionException directly and refuse to execute new tasks.
  • CallerRunsPolicy: The task is returned to the caller for execution, and the task is executed by the thread that submitted the task.
  • DiscardPolicy: Directly discard tasks that cannot be executed without any exception being thrown.
  • DiscardOldestPolicy: Discard the oldest task in the queue and retry the new task.

The rejection policy can be set through the setRejectedExecutionHandler() method:

executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
  1. Optionally set the thread factory (optional):
    The thread factory is used to create new threads, and the default thread factory (DefaultThreadFactory) is used by default. If you need to customize the thread factory, you can implement the ThreadFactory interface and set it through the setThreadFactory() method:
executor.setThreadFactory(runnable -> new Thread(runnable, "MyThread"));

3. The core components of the thread pool:

Introduction to the ThreadPoolExecutor class

ThreadPoolExecutor is a class in Java for creating and managing thread pools. It is a specific implementation of the ExecutorService interface, which provides flexible thread pool parameter configuration and task execution management.

The constructor of the ThreadPoolExecutor class is as follows:

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

Parameter Description:

  • corePoolSize: The number of core threads in the thread pool. When a new task is submitted to the thread pool, if the current number of threads is less than corePoolSize, a new thread will be created immediately to process the task;
  • maximumPoolSize: The maximum number of threads allowed by the thread pool. When the queue is full and the current number of threads is less than maximumPoolSize, a new thread will be created to process the task;
  • keepAliveTime: The survival time of redundant idle threads, that is, when the number of threads in the thread pool is greater than corePoolSize, the redundant idle threads will be destroyed if they are not used within a certain period of time;
  • unit: the time unit of keepAliveTime;
  • workQueue: A blocking queue used to store tasks to be executed.

ThreadPoolExecutor provides the following methods to manage thread pools:

  1. execute(Runnable command): Submit a Runnable task to the thread pool for execution. This method is non-blocking, and the task will be added to the work queue for execution.

  2. submit(Callable task): Submit a Callable task to the thread pool for execution, and return a Future object representing the result of the task.

  3. shutdown(): Shut down the thread pool smoothly. The thread pool will no longer accept new tasks, but will wait for submitted tasks to complete before closing.

  4. shutdownNow(): Close the thread pool immediately. The thread pool will attempt to interrupt executing tasks and return a list of unexecuted tasks.

  5. setCorePoolSize(int corePoolSize): Set the number of core threads in the thread pool.

  6. setMaximumPoolSize(int maximumPoolSize): Set the maximum number of threads in the thread pool.

  7. setKeepAliveTime(long keepAliveTime, TimeUnit unit): Set the survival time of idle threads.

  8. setThreadFactory(ThreadFactory threadFactory): Set the thread factory for creating new threads.

  9. setRejectedExecutionHandler(RejectedExecutionHandler handler): Set the rejection strategy, when the work queue is full and cannot accept new tasks, decide how to handle tasks that cannot be executed.

  10. getQueue(): Get the work queue used by the thread pool.

  11. getActiveCount(): Get the number of currently active threads.

  12. getCompletedTaskCount(): Get the number of completed tasks.

  13. getTaskCount(): Get the number of submitted tasks.

  14. isShutdown(): Determine whether the thread pool has been closed.

  15. isTerminated(): Determines whether the thread pool has terminated.

Setting of core thread pool size, maximum thread pool size and thread survival time

In ThreadPoolExecutor, the core thread pool size (corePoolSize), maximum thread pool size (maximumPoolSize) and thread survival time (keepAliveTime) are very important parameters, which can be set according to actual needs.

Core thread pool size (corePoolSize):

The core thread pool size specifies the number of threads kept active in the thread pool. When a new task is submitted to the thread pool, if the current number of threads is less than the size of the core thread pool, a new thread will be created immediately to process the task. If the number of threads in the thread pool has reached the core thread pool size, the new task will be put into the work queue for execution.

The setting of the core thread pool size needs to be reasonably estimated according to the type of tasks and load conditions. If the execution time of the task is long or the number of tasks is large, the size of the core thread pool can be appropriately increased to improve the response speed of the task.

Maximum thread pool size (maximumPoolSize):

The maximum thread pool size specifies the maximum number of threads allowed by the thread pool. When the work queue is full and the current number of threads is less than the maximum thread pool size, new threads are created to process tasks. If the work queue is full and the current number of threads has reached the maximum thread pool size, unexecutable tasks are processed according to the rejection policy.

The setting of the maximum thread pool size needs to be adjusted reasonably according to system resources, nature of tasks and load conditions. If the number of tasks often exceeds the core thread pool size, the maximum thread pool size can be appropriately increased to handle peak tasks.

Thread survival time (keepAliveTime):

The thread lifetime specifies how long redundant idle threads survive before being destroyed. When the number of threads in the thread pool exceeds the size of the core thread pool, the redundant idle threads will be destroyed when they are not used within a certain period of time.

The setting of the thread survival time can be adjusted according to the duration of the task and business requirements. If the execution time of the task is short, the thread survival time can be set to a short time, so as to release redundant thread resources in time. If the execution time of the task is long, you can set the thread survival time to a longer time to avoid frequently creating and destroying threads.

It should be noted that in ThreadPoolExecutor, by default, idle threads will be destroyed according to the thread survival time only after the core thread pool size is full. You can set whether core threads can also be recycled by calling the allowCoreThreadTimeOut(boolean value) method.

Choice of task queue and rejection strategy

Task queues and rejection policies are very important concepts in ThreadPoolExecutor, which play a vital role in the performance and stability of thread pools.

  1. Task queue:
    The task queue (workQueue) is used to store tasks to be executed. When the number of threads in the thread pool has reached the size of the core thread pool, new tasks will be put into the task queue for execution until the queue is full. The thread pool provides various types of task queues, the common ones are as follows:
  • Direct submission queue (SynchronousQueue): This is a queue with no capacity and can only store one task at a time. If no thread is currently available in the thread pool to perform the task, a new thread is created immediately. Applicable to scenarios with light load and short task execution time.

  • Bounded queues (ArrayBlockingQueue, LinkedBlockingQueue): These queues have a fixed capacity and can store a certain number of tasks. Once the queue is full, new tasks wait until a thread becomes available for execution. It can be set reasonably according to the amount of tasks and system resources. ArrayBlockingQueue is an array-based bounded queue, and LinkedBlockingQueue is a linked-list-based bounded (or unbounded) queue.

  • Unbounded queue (LinkedBlockingQueue): This is a queue with optional capacity, which can store any number of tasks. Tasks are added to the queue as long as system resources allow, until the queue runs out of system memory. Applicable to scenarios with a large amount of tasks and sufficient system resources.

According to specific needs and scenarios, we can choose the appropriate task queue type to manage the tasks to be executed.

  1. Rejection policy:
    When the task queue is full and the number of threads in the thread pool has reached the maximum thread pool size, new tasks cannot be added to the queue. At this time, you need to use the rejection strategy to decide how to deal with the tasks that cannot be performed. ThreadPoolExecutor provides a variety of predefined rejection strategies, as well as the option to customize the rejection strategy.
  • AbortPolicy (default): Throw RejectedExecutionException directly to prevent task submission.

  • CallerRunsPolicy: Return tasks to the caller for processing. In interactive applications, this strategy may cause the main thread to be blocked, but it guarantees that no tasks will be lost.

  • DiscardPolicy: Silently discard tasks that cannot be executed, without providing any feedback.

  • DiscardOldestPolicy: Discard the oldest task in the queue, then try to submit a new task again.

In addition to the predefined rejection strategies, you can also customize the rejection strategy by implementing the RejectedExecutionHandler interface, and perform specific processing according to business needs.

Fourth, the task execution of the thread pool:

Use of Callable and Future

Callable and Future are interfaces in Java multithreaded programming for processing tasks with return results. A task with a return value can be defined through the Callable interface, and the Future interface is used to represent the execution result of the task.

  1. Define Callable task:
    First, you need to define a task class that implements the Callable interface. There is only one method call() in the Callable interface, which is used to perform a task and return a result.
import java.util.concurrent.Callable;

class MyCallableTask implements Callable<String> {
    
    
    @Override
    public String call() throws Exception {
    
    
        // 执行任务逻辑并返回结果
        return "任务执行结果";
    }
}
  1. Submit the Callable task to the thread pool:
    use the submit() method of ThreadPoolExecutor to submit the Callable task to the thread pool for execution, and return a Future object to represent the execution result of the task.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<String> task = new MyCallableTask();
Future<String> future = executor.submit(task);
  1. Processing task execution result:
    the execution result of the task can be obtained through the Future object. The Future interface provides multiple methods for processing task execution results:
  • get(): Block the current thread until the task execution is completed and the result is returned. If the task has not been completed, the get() method will wait forever. The waiting time can be set by the get(long timeout, TimeUnit unit) method.
String result = future.get();  // 获取任务执行结果
  • isDone(): Determines whether the task has been completed. Returns true if the task completed, even if an exception was thrown.
boolean isDone = future.isDone();  // 判断任务是否完成
  • cancel(): Attempts to cancel the execution of the task. If the task has not yet started, the task will be successfully canceled; if the task has already started or completed, the task cannot be canceled.
boolean isCancelled = future.cancel(true);  // 尝试取消任务
  • isCancelled(): Determine whether the task has been canceled.
boolean isCancelled = future.isCancelled();  // 判断任务是否被取消

It should be noted that calling the get() method will block the current thread until the task execution is completed and the result is returned. If you don't want to block the current thread, you can use the isDone() method to determine whether the task has been completed, or use the get(long timeout, TimeUnit unit) method to set the waiting timeout period.

Through the Callable and Future interfaces, you can easily define tasks with return values ​​and obtain the execution results of the tasks. This is very useful in scenarios where time-consuming calculations and concurrent data requests are required.

Execute scheduled tasks and periodic tasks

In Java, you can use the interface java.util.concurrentin the package ScheduledExecutorServiceto perform timing tasks and periodic tasks. ScheduledExecutorServiceThe following methods are provided to submit scheduled tasks and periodic tasks:

  1. schedule(Runnable task, long delay, TimeUnit unit):
    Execute the task once after the given delay.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
    
    
    // 执行任务逻辑
};
executor.schedule(task, 5, TimeUnit.SECONDS);  // 延迟 5 秒后执行任务
  1. schedule(Callable<V> task, long delay, TimeUnit unit):
    Execute a Callable task after a given delay, and return a Future object for obtaining the execution result of the task.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Callable<String> task = () -> {
    
    
    // 执行任务逻辑并返回结果
    return "任务执行结果";
};
ScheduledFuture<String> future = executor.schedule(task, 5, TimeUnit.SECONDS);  // 延迟 5 秒后执行任务
// 获取任务执行结果
String result = future.get();
  1. scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit):
    Start executing the task periodically after a given initial delay, with a fixed wait for the given interval between each execution.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
    
    
    // 执行任务逻辑
};
executor.scheduleAtFixedRate(task, 1, 5, TimeUnit.SECONDS);  // 初始延迟 1 秒后开始执行任务,每次执行之间间隔 5 秒
  1. scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit):
    Start executing tasks periodically after a given initial delay, and wait for a given time interval after each task execution is completed before executing the next one.
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
Runnable task = () -> {
    
    
    // 执行任务逻辑
};
executor.scheduleWithFixedDelay(task, 1, 5, TimeUnit.SECONDS);  // 初始延迟 1 秒后开始执行任务,每次任务完成后等待 5 秒再执行下一次

It should be noted that the time parameter in the above method can use TimeUnitthe enumeration class to specify the time unit, for example TimeUnit.SECONDSmeans seconds. In addition, a method ScheduledExecutorServiceis provided shutdown()to close the thread pool and release resources when there is no need to execute scheduled tasks and periodic tasks.

executor.shutdown();

5. Status and monitoring of thread pool:

The running state and conversion process of the thread pool

The running state and conversion process of the thread pool are mainly managed by the (control state) field ThreadPoolExecutorin the class . ctlThis field uses a variable of type int to represent the status of the thread pool and the number of active threads.

The running status of the thread pool includes the following:

  1. RUNNING (running):

    • The initial state of the thread pool, accepting new tasks and processing tasks that have been added to the queue.
    • The corresponding value is: RUNNING = -1 << COUNT_BITS(the upper 29 bits are 0, the lower 3 bits are 111)
  2. SHUTDOWN (closed):

    • No new tasks will be accepted, but tasks that have been added to the queue will continue to be processed.
    • The corresponding value is: SHUTDOWN = 0 << COUNT_BITS(the upper 29 bits are 0, the lower 3 bits are 000)
  3. STOP (stopping):

    • No new tasks will be accepted, tasks already added to the queue will not be processed, and ongoing tasks will be interrupted.
    • The corresponding value is: STOP = 1 << COUNT_BITS(the upper 29 bits are 0, the lower 3 bits are 001)
  4. TIDYING (finishing):

    • All tasks have been terminated, and the number of active threads is 0. After entering this state, terminated()the method will be executed for some follow-up operations.
    • The corresponding value is: TIDYING = 2 << COUNT_BITS(the upper 29 bits are 0, the lower 3 bits are 010)
  5. TERMINATED:

    • The thread pool is completely terminated. After being in this state, the thread pool will no longer change state.
    • The corresponding value is: TERMINATED = 3 << COUNT_BITS(the upper 29 bits are 0, the lower 3 bits are 011)

The conversion process is as follows:

  1. Initial state:RUNNING

  2. shutdown()The method is called and enters SHUTDOWNthe state:

    • No new tasks will be accepted, but tasks that have been added to the queue will continue to be processed.
  3. shutdownNow()The method is called and enters STOPthe state:

    • No new tasks are accepted, nor are tasks already added to the queue processed, interrupting ongoing tasks.
  4. After all tasks are completed, the number of active threads is 0 and enters TIDYINGthe state:

    • Execute terminated()the method to perform some follow-up operations, such as closing the thread pool.
  5. The thread pool is completely terminated and enters TERMINATEDthe state:

    • Once in this state, the thread pool will no longer change state.

Monitor the number of active threads in the thread pool and the status of the task queue

To monitor the number of active threads in the thread pool and the state of the task queue, you can use ThreadPoolExecutorsome methods and properties provided by the class.

  1. Number of active threads:

    • Use getActiveCount()the method to get the number of currently active threads in the thread pool.
    • This method returns the number of threads currently executing tasks, excluding idle threads.
  2. Task Queue Status:

    • Use getQueue()the method to get the task queue used by the thread pool.
    • LinkedBlockingQueueis ThreadPoolExecutorthe default queue implementation used, which is an unbounded blocking queue.
    • size()The number of tasks in the queue can be obtained through the method.
    • If you are using other types of queues, you can obtain the queue status according to the corresponding method.

The sample code is as follows:

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,   // 核心线程数
    maximumPoolSize,   // 最大线程数
    keepAliveTime, TimeUnit.SECONDS,   // 线程空闲时间
    new LinkedBlockingQueue<Runnable>()   // 任务队列
);

// 获取活动线程数
int activeThreadCount = executor.getActiveCount();

// 获取任务队列状态
BlockingQueue<Runnable> queue = executor.getQueue();
int taskCount = queue.size();
int remainingCapacity = queue.remainingCapacity();  // 队列剩余容量

Note that the number of active threads and task queue status obtained by the above methods are immediate. In a multi-threaded environment, these values ​​may change. Therefore, if you need to monitor the number of active threads in the thread pool and the status of the task queue, you should call these methods periodically to obtain the latest values.

Monitoring and custom actions using the monitor interface and hook methods

In Java, you can use the monitor interface (Monitor Interface) and the hook method (Hook Method) to monitor and customize the operation of the thread pool.

  1. Monitor interface:

    • The Monitor interface defines callback methods for notifying listeners when certain events occur.
    • Java provides ThreadPoolExecutorsubclasses of the class ThreadPoolExecutorMBean, which implements java.util.concurrent.ThreadPoolExecutorMBeanthe interface.
    • By registering the implementation class of the monitor interface, you can receive event notifications at various stages of the thread pool.
  2. Hook method:

    • A hook method refers to a method that is called at a specific point in time and can be overridden by subclasses to customize some operations.
    • Java provides ThreadPoolExecutorthree rewritable hook methods of the class: beforeExecute(), afterExecute()and terminated().
    • These hook methods allow you to perform some custom actions before and after each task in the thread pool is executed, and when the thread pool terminates.

The following is a detailed introduction to these interfaces and methods:

  1. How to use the monitor interface:

    • Create a class that implements java.util.concurrent.ThreadPoolExecutorMBeanthe interface and implement the callback method in it.
    • Register this implementation class with MBean Server, through which you can obtain and manage thread pool related information.
    • Monitor events that can be received include thread pool state transitions, changes in the number of active threads, and the status of task queues.
  2. How to use the hook method:

    • beforeExecute(Thread, Runnable): is called before each task is executed.
      • It can be used to record the execution information of the task, set some context, etc.
    • afterExecute(Runnable, Throwable): is called after each task execution.
      • Can be used for resource cleanup, logging, exception handling, etc.
    • terminated(): Called when the thread pool terminates.
      • It can be used to perform some operations after the thread pool terminates, such as releasing resources, sending notifications, etc.

The sample code is as follows:

public class MyThreadPool extends ThreadPoolExecutor {
    
    

    public MyThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit,
            BlockingQueue<Runnable> workQueue) {
    
    
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    protected void beforeExecute(Thread t, Runnable r) {
    
    
        // 在任务执行之前进行一些自定义操作
        System.out.println("Task " + r.toString() + " is about to execute.");
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
    
    
        // 在任务执行之后进行一些自定义操作
        System.out.println("Task " + r.toString() + " has finished.");
        if (t != null) {
    
    
            System.out.println("An exception occurred during task execution: " + t.getMessage());
        }
    }

    @Override
    protected void terminated() {
    
    
        // 在线程池终止时进行一些自定义操作
        System.out.println("ThreadPool has terminated.");
    }
}

// 创建线程池实例
MyThreadPool threadPool = new MyThreadPool(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>());

// 提交任务
threadPool.execute(task);

// 关闭线程池
threadPool.shutdown();

By overriding hook methods, custom actions can be performed at appropriate points in time. In this way, thread pool monitoring and some additional actions can be realized, such as logging, exception handling, resource release, etc.

6. Combination of concurrent tool class and thread pool:

Use of CountDownLatch

CountDownLatch (countdown latch) is a synchronization tool class in the Java concurrent package, which allows one or more threads to wait until the specified count is reached.

CountDownLatch has two main methods:

  1. countDown()method:

    • Each time countDown()the method is called, the counter is decremented by 1.
    • When the counter decreases to 0, all waiting threads will be released.
  2. await()method:

    • The thread calling await()the method waits until the counter is decremented to zero.
    • If the counter is already at 0, it returns immediately.

Usage scenarios of CountDownLatch:

  • The main thread waits for all child threads to complete before continuing.
  • Coordinate multiple concurrent operations to ensure that one operation is executed after other operations complete.

Here is an example using CountDownLatch and thread pool:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Example {
    
    

    public static void main(String[] args) throws InterruptedException {
    
    
        int threadCount = 5;
        CountDownLatch latch = new CountDownLatch(threadCount);
        ExecutorService executorService = Executors.newFixedThreadPool(threadCount);

        // 提交任务到线程池
        for (int i = 0; i < threadCount; i++) {
    
    
            executorService.execute(new Worker(latch));
        }

        // 等待所有任务完成
        latch.await();

        // 关闭线程池
        executorService.shutdown();

        System.out.println("All workers have completed their tasks.");
    }

    static class Worker implements Runnable {
    
    
        private final CountDownLatch latch;

        public Worker(CountDownLatch latch) {
    
    
            this.latch = latch;
        }

        @Override
        public void run() {
    
    
            try {
    
    
                // 模拟任务执行
                Thread.sleep((long) (Math.random() * 5000));
                System.out.println("Worker thread has completed its task.");
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                // 完成任务后调用 countDown() 方法
                latch.countDown();
            }
        }
    }
}

In the above example, we created a thread pool with 5 threads and submitted tasks to the thread pool. As each task completes, countDown()the method is called to decrement the counter. The main thread calls latch.await()to wait for all tasks to complete, and when the counter decreases to 0, the main thread continues execution and prints "All workers have completed their tasks.".

It should be noted that when using the thread pool, we need to manually call countDown()the method after the task is completed to ensure that the counter is decremented. In addition, after using the thread pool, shutdown()the method should be called to close the thread pool.

By using CountDownLatch and thread pool, we can easily realize the synchronization and cooperation between tasks, so that the main thread can continue to execute after all tasks are completed.

Use of CyclicBarriers

CyclicBarrier (cyclic barrier) is a synchronization tool class in the Java concurrent package, which allows a group of threads to wait before reaching a specified number, and can be reused.

CyclicBarrier has two main construction methods:

  1. CyclicBarrier(int parties): Specify the number of threads that need to wait, that is, the number of parties.
  2. CyclicBarrier(int parties, Runnable barrierAction): In addition to specifying the number of threads that need to wait, you can also specify the actions that need to be performed when all threads reach the barrier.

CyclicBarrier has two main methods:

  1. await(): The calling thread waits at the barrier until parties number of threads have called the method.
  2. await(long timeout, TimeUnit unit): await()Similar to the method, but you can specify the maximum waiting time.

CyclicBarrier usage scenarios:

  • In parallel tasks, a task needs to wait for all other tasks to complete before continuing, similar to waiting for the results of other threads.
  • In the task decomposition and calculation scenario, a large task is split into multiple subtasks, and each subtask is executed separately, and then the results are summarized after all subtasks are completed.

Here is an example using CyclicBarrier:

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class Example {
    
    

    public static void main(String[] args) {
    
    
        int parties = 3;
        CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
    
    
            System.out.println("All parties have reached the barrier.");
        });

        for (int i = 0; i < parties; i++) {
    
    
            new Thread(new Worker(barrier)).start();
        }
    }

    static class Worker implements Runnable {
    
    
        private final CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
    
    
            this.barrier = barrier;
        }

        @Override
        public void run() {
    
    
            try {
    
    
                // 模拟任务执行
                Thread.sleep((long) (Math.random() * 5000));
                System.out.println(Thread.currentThread().getName() + " has reached the barrier.");

                // 等待其他线程到达屏障
                barrier.await();

                System.out.println(Thread.currentThread().getName() + " continues to execute.");
            } catch (InterruptedException | BrokenBarrierException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

In the above example, we create a CyclicBarrier, specify the number of threads to wait as 3, and provide an action to be executed when all threads have reached the barrier. We then create 3 threads (number of parties), each of which performs some task and at some point calls a barrier.await()method to wait for the other threads. When all threads have reached the barrier, the action will be executed.

It should be noted that CyclicBarrier is reusable. Every time all threads have reached the barrier, the counter is reset and a new round of waiting begins.

By using CyclicBarrier, we can achieve synchronization between multiple threads, so that these threads can wait until the specified number is reached, and continue to perform subsequent operations after reaching the number. This is useful for scenarios where tasks are split and results are merged.

Use of Semaphore

Semaphore (semaphore) is a synchronization tool class in the Java concurrency package, which can be used to control the number of threads accessing a resource at the same time.

Semaphore limits the number of threads that can simultaneously access a resource by maintaining a set of permits. A thread can acquire a license through the acquire() method, and if the number of licenses is insufficient, the thread will be blocked. After the thread finishes using the resource, it can release the license through the release() method, so that other threads waiting for the license can continue to execute.

Semaphore has two main construction methods:

  1. Semaphore(int permits): Specifies the number of initial licenses.
  2. Semaphore(int permits, boolean fair): In addition to specifying the number of initial licenses, you can also specify fairness, that is, whether to allocate licenses in a first-in, first-out order.

Semaphore has three core methods:

  1. acquire(): Acquires a license, if no license is available, the calling thread will be blocked.
  2. acquire(int permits): Get the specified number of licenses, if there are not enough licenses, the calling thread will be blocked.
  3. release(): Release a license.
  4. release(int permits): Release the specified number of licenses.

Semaphore usage scenarios:

  • Control the number of threads accessing a resource at the same time, such as connection pool, thread pool, etc.
  • Implement current-limiting operations and limit the number of concurrency of an operation.

Here is an example using Semaphore:

import java.util.concurrent.Semaphore;

public class Example {
    
    

    public static void main(String[] args) {
    
    
        int permits = 2; // 允许同时访问的线程数量
        Semaphore semaphore = new Semaphore(permits);

        for (int i = 0; i < 5; i++) {
    
    
            new Thread(new Worker(semaphore)).start();
        }
    }

    static class Worker implements Runnable {
    
    
        private final Semaphore semaphore;

        public Worker(Semaphore semaphore) {
    
    
            this.semaphore = semaphore;
        }

        @Override
        public void run() {
    
    
            try {
    
    
                // 获取许可证
                semaphore.acquire();
                
                // 模拟任务执行
                System.out.println(Thread.currentThread().getName() + " is executing.");

                // 释放许可证
                semaphore.release();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
        }
    }
}

In the above example, we created a Semaphore, specifying the number of initial licenses as 2. Then, we create 5 threads, each thread semaphore.acquire()obtains a license through the method before executing the task, if no license is available, the thread will be blocked. After the task is executed, the thread semaphore.release()releases the license through the method.

It should be noted that the number of Semaphore licenses will change dynamically, the acquire() method will decrease the count when acquiring the license, and the release() method will increase the count when releasing the license. If at some point the number of permits is 0 and all threads are waiting, then as soon as one thread releases the permit, there will be a waiting thread that acquires the permit.

By using Semaphore, we can effectively limit the number of threads that access a certain resource at the same time, thereby achieving concurrency control and current limiting operations.

Use of CompletableFuture

CompletableFuture is a class in the Java concurrent package, which provides a way to simplify asynchronous programming, and can be used to execute asynchronous tasks and process task results.

CompletableFuture can programmatically create, compose and execute asynchronous tasks, while supporting chained operations and callback functions. It can trigger subsequent actions when a task is complete, or it can trigger an action when all tasks are complete. CompletableFuture can be used to process the results of asynchronous tasks more conveniently, avoiding the complexity of traditional callback style code writing.

The main features and usage of CompletableFuture are as follows:

  1. Create CompletableFuture

    • CompletableFuture.runAsync(Runnable runnable): Creates a CompletableFuture and executes the given Runnable tasks asynchronously.
    • CompletableFuture.supplyAsync(Supplier<U> supplier): Create a CompletableFuture and execute the given Supplier task asynchronously, and return the calculation result.
  2. What to do when the async task completes

    • thenApply(Function<? super T,? extends U> fn): Applies the given function after the previous task is completed, and returns a new CompletableFuture.
    • thenAccept(Consumer<? super T> action): Applies the given consumption function after the previous task is completed, with no return value.
    • thenRun(Runnable action): Executes the given Runnable after the previous task is completed, with no return value.
  3. Callback function when the asynchronous task is completed

    • whenComplete(BiConsumer<? super T,? super Throwable> action): Execute the given callback function after the previous task is completed, no matter whether an exception occurs or not.
    • exceptionally(Function<Throwable,? extends T> fn): Execute the given callback function when an exception occurs in the previous task, and return a new CompletableFuture.
  4. Combining multiple CompletableFutures

    • thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn): When two CompletableFutures complete, apply the given function to process the results and return a new CompletableFuture.
    • thenCompose(Function<? super T,? extends CompletionStage<U>> fn): After the previous task is completed, pass the result to the given function and return a new CompletableFuture.
  5. Wait for all CompletableFutures to complete

    • allOf(CompletableFuture<?>... cfs): Returns a new CompletableFuture that triggers an action when all given CompletableFutures complete.
    • anyOf(CompletableFuture<?>... cfs): Returns a new CompletableFuture that triggers an action when any given CompletableFuture completes.

Here's an example using CompletableFuture:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class Example {
    
    

    public static void main(String[] args) throws ExecutionException, InterruptedException {
    
    
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> "Hello");
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> " World!");

        CompletableFuture<String> combinedFuture = future1.thenCombine(future2, (result1, result2) -> result1 + result2);
        System.out.println(combinedFuture.get());
    }
}

In the example above, we create two CompletableFutures of asynchronous tasks that return the strings "Hello" and "World!" respectively. Then, we thenCombine()combine the two CompletableFutures through the method, and when the two CompletableFutures are completed, their results are concatenated and returned. Finally, we use get()the method to obtain the final calculation result.

It should be noted that CompletableFuture is variable, which allows us to perform a series of operations on the result after the asynchronous task is completed, including data conversion, callback function and combination of multiple CompletableFutures. It provides a more concise and flexible way to handle the results and flow control of asynchronous tasks.

7. Parallel streams and thread pools in Java 8+:

Parallel stream concepts and how they work

Parallel Streams is a facility introduced in Java 8 for parallel processing of collection data. It can convert a sequential stream (Sequential Stream) into a parallel stream (Parallel Stream), thus using multithreading to speed up the processing of aggregated data.

Parallel streams concept and work as follows:

  1. Concept:
    Parallel streaming is a parallel processing method for collection data. It divides the collection into multiple small pieces and uses multiple threads to process the data of these small pieces at the same time. Through parallel processing, the efficiency of processing large amounts of data can be improved.

  2. How it works:
    First, the aggregate data needs to be converted to a sequential stream before using the parallel stream. A sequence stream can be obtained through stream()the method or methods in the collection class such parallelStream()as . A sequential stream is a stream that processes elements one by one, and its operations are performed serially.

    Then, parallel()convert the sequential stream to a parallel stream by calling the method. Parallel stream operations are performed simultaneously in multiple threads to achieve parallel processing of aggregated data.

    Parallel streams work by breaking data into small chunks and assigning each chunk to a different thread for processing. Each thread independently processes the small blocks allocated to it, and combines the processing results to finally obtain the overall processing result. In this way, multi-core processors and multi-threads can be fully utilized to speed up the processing of aggregated data.

    The specific workflow of the parallel stream is as follows:

    • Divide collection data into multiple small chunks.
    • Create a task for each chunk and assign the tasks to different threads for processing.
    • Threads independently process their assigned chunks and generate intermediate results.
    • Merge the intermediate results of all threads to get the final processing result.

It should be noted that the use of parallel streams requires attention to the following points:

  • Parallel streams are suitable for processing large amounts of data, complex calculations, or time-consuming operations. Sequential streams are more efficient for processing simple tasks.
  • When using parallel streams, you need to consider the impact of data splitting and merging on performance. If the amount of data is too small or the overhead of data splitting and merging is too high, the performance of parallel streams may not be as good as that of sequential streams.
  • When using parallel streams, you need to pay attention to the problem of shared state. Parallel stream operations are performed by multiple threads at the same time, so the modification of the shared state needs to be synchronized or use concurrent containers to ensure thread safety.

Improving the performance of parallel streams using thread pools

Using a thread pool can further improve the performance of parallel streams. The thread pool is a mechanism for managing and reusing threads. It can effectively control the number of concurrent threads, and provide thread reuse and scheduling functions, thereby reducing the overhead of thread creation and destruction.

The following is a detailed description of how to use the thread pool to improve the performance of parallel streams:

  1. Create a thread pool:
    First, you need to create a thread pool to manage concurrently executed tasks. You can use Executors.newFixedThreadPool()the method to create a thread pool with a fixed size, or use Executors.newCachedThreadPool()the method to create a thread pool that automatically adjusts its size as needed.

  2. Use the thread pool to execute tasks in parallel:
    use the parallel stream parallelStream()method to convert sequential streams into parallel streams, and use the thread pool to execute parallel tasks. withExecutor()A custom thread pool can be passed to a parallel stream via the method.

    For example, a thread pool can be used like this to perform parallel stream operations:

    ExecutorService executorService = Executors.newFixedThreadPool(4);
    List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
    
    list.parallelStream()
        .withExecutor(executorService)
        .forEach(System.out::println);
    
    executorService.shutdown();
    
  3. Control the size of the thread pool:
    By setting the size of the thread pool, you can control the number of threads that execute tasks simultaneously in the parallel stream. The size of the thread pool depends on the number of processor cores available, available memory, and the nature of the tasks.

    Generally speaking, if the task is CPU intensive, the size of the thread pool can be set to the number of processor cores. And if the task is IO-intensive, the size of the thread pool can be appropriately increased according to the actual situation to improve the parallelism.

  4. Close the thread pool:
    After using the thread pool, you need to call shutdown()the method to close the thread pool and release resources. You can close the thread pool manually after all tasks are completed, or use shutdownNow()the method to close the thread pool immediately.

Using a thread pool can improve the performance of parallel streams for the following reasons:

  • The thread pool can reuse threads, avoiding the overhead of thread creation and destruction, and reducing the consumption of system resources.
  • The thread pool can control the number of concurrent threads, avoiding the thread scheduling overhead and resource competition caused by starting a large number of threads at the same time.
  • The thread pool can provide thread scheduling and management functions, making the execution of concurrent tasks more controllable and flexible.

Guess you like

Origin blog.csdn.net/u012581020/article/details/132142232