16 How to Customize Your Own Thread Pool Based on Actual Needs

16 How to Customize Your Own Thread Pool Based on Actual Needs #

In this lesson, we mainly learn how to customize our own thread pool by setting various parameters according to our actual needs.

Core Thread Count #

The first parameter that needs to be set is often the corePoolSize. As we discussed in the previous lesson, the reasonable number of threads is related to the type of tasks, the number of CPU cores, etc. The basic conclusion is that the higher the proportion of the average working time of the threads, the fewer threads are needed; the higher the proportion of the average waiting time of the threads, the more threads are needed. As for the maximum thread count, if the type of tasks we execute is not fixed, for example, it may be CPU-intensive for a period of time, I/O-intensive for another period of time, or a mixture of both types of tasks. In this case, we can set the maximum thread count to several times the core thread count to cope with task bursts. Of course, a better solution is to use different thread pools to execute different types of tasks, so that tasks can be differentiated by type rather than mixed together. In this way, we can set the appropriate number of threads based on the estimated number of threads from the previous lesson or the results obtained from stress testing, achieving better performance.

Blocking Queue #

As for the BlockingQueue, we can choose the LinkedBlockingQueue or SynchronousQueue or DelayedWorkQueue that we introduced earlier. However, there is another commonly used blocking queue called ArrayBlockingQueue, which is also often used in thread pools. This type of blocking queue is implemented internally using an array. When creating an object, a capacity value needs to be passed in, and the array cannot be resized afterwards. Therefore, the most significant feature of ArrayBlockingQueue is that its capacity is limited. In this way, if the task queue is full and the number of threads has reached the maximum value, the thread pool will reject newly submitted tasks according to the rules, resulting in possible data loss.

However, compared to an unlimited increase in tasks or thread count leading to insufficient memory and program crashes, data loss is still relatively better. If we use ArrayBlockingQueue as the blocking queue and limit the maximum thread count, we can effectively prevent resource exhaustion. The queue capacity and maxPoolSize are a trade-off. If we use a larger queue capacity and a smaller maximum thread count, we can reduce the overhead caused by context switching, but it may also reduce the overall throughput. If our tasks are I/O-intensive, we can choose a slightly smaller queue capacity and a larger maximum thread count, which will result in higher overall efficiency but also more context switching.

Thread Factory #

For the threadFactory parameter, we can use the default defaultThreadFactory, or we can pass in a custom thread factory with additional capabilities. Since we may have multiple thread pools and it is necessary to differentiate them using different names, we can pass in a thread factory that can be named based on business information, so that we can quickly locate problem code based on the thread name. For example, we can use com.google.common.util.concurrent.ThreadFactoryBuilder to achieve this, as shown in the code snippet.

ThreadFactoryBuilder builder = new ThreadFactoryBuilder();

ThreadFactory rpcFactory = builder.setNameFormat("rpc-pool-%d").build();

We generate a ThreadFactory named rpcFactory, and its nameFormat is "rpc-pool-%d". In this way, the names of the threads it generates have a fixed format, which are “rpc-pool-1”, “rpc-pool-2”, and so on.

Rejection Policy #

The last parameter is the rejection policy. We can choose one of the four rejection policies mentioned in Lesson 11: AbortPolicy, DiscardPolicy, DiscardOldestPolicy, or CallerRunsPolicy, according to business needs. In addition to these, we can also implement our own rejection policy by implementing the RejectedExecutionHandler interface. In the interface, we need to implement the rejectedExecution method, where we can perform custom rejection policies such as printing logs, temporarily storing tasks, or re-executing. This can meet business needs. As shown in the code snippet:

private static class CustomRejectionHandler implements RejectedExecutionHandler {

    @Override

    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

        // Custom rejection policy: print logs, temporarily store tasks, re-execute, etc.

    }

}

Summary #

Therefore, customizing our own thread pool is strongly related to our business. First, we need to understand the meaning of each parameter and the common options, and then customize a thread pool that is very suitable for our business based on our actual needs, such as concurrency, memory size, whether to accept rejected tasks, and a series of factors. This way, it will not cause insufficient memory, and we can use an appropriate number of threads to ensure the efficiency of task execution. When rejecting tasks, we can also keep records for future tracing.