Understanding Java Thread Pools: Core Concepts, Rejection Policies, and Sizing Guidelines
This article explains Java thread pool fundamentals—including core parameters, task submission behavior, rejection policies, queue selection, thread factory usage, keep-alive settings—and provides practical guidance on determining optimal thread counts for I/O‑bound and CPU‑bound workloads.
1. Thread Pool Basic Working Principle and Interview Guide
1.1 Core attributes of Java thread pool
Java thread pools expose several core attributes:
int corePoolSize – number of core threads
int maximumPoolSize – maximum number of threads
long keepAliveTime – time that excess threads may stay idle before termination
TimeUnit unit – time unit for keepAliveTime
BlockingQueue<Runnable> workQueue – task queue
ThreadFactory threadFactory – factory for creating new threads
RejectedExecutionHandler handler – rejection policy
1.2 Thread creation process when submitting tasks
When a task is submitted, the pool follows these steps:
If the current number of threads is less than corePoolSize , a new thread is created to execute the task, regardless of whether existing threads are idle.
When the number of threads reaches corePoolSize , the pool checks the task queue: 1) If the queue is not full, the task is enqueued. 2) If the queue is full, the pool checks whether the current thread count is below maximumPoolSize . If so, a new thread is created; otherwise, the rejection policy is applied.
Tip: Using an unbounded queue makes maximumPoolSize ineffective because the pool will never need to create threads beyond the core size.
1.3 Rejection policies and usage scenarios
JUC provides four built‑in rejection policies:
AbortPolicy – throws RejectedExecutionException (default).
CallerRunsPolicy – the calling thread runs the task, turning async execution into sync.
DiscardOldestPolicy – discards the oldest task in the queue.
DiscardPolicy – silently drops the new task.
The rejection policy is triggered only when a bounded queue is full and the pool has already reached maximumPoolSize. In most cases, AbortPolicy is sufficient. CallerRunsPolicy rarely fits real scenarios because it adds load to the caller. DiscardOldestPolicy is useful for non‑critical logging or tracing where losing older data is acceptable. DiscardPolicy is suitable for fire‑and‑forget logging where loss is tolerable.
1.4 How to choose a blocking queue
Alibaba’s internal open‑source guidelines explicitly forbid using unbounded queues because they can cause memory overflow when tasks are submitted faster than they are processed.
If an unbounded queue is used, the maximumPoolSize parameter becomes meaningless, as the pool will never create more than corePoolSize threads.
1.5 Practical use of thread factory
Defining a custom ThreadFactory allows you to name threads, which greatly simplifies debugging with tools like jstack by making thread stacks easily identifiable.
1.6 Role of keepAliveTime
keepAliveTimespecifies the maximum idle time for non‑core threads before they are terminated. By default it only applies to threads beyond corePoolSize, but setting allowCoreThreadTimeOut(true) makes it affect core threads as well.
2. How to Set an Appropriate Number of Threads for a Thread Pool
Common sizing formulas are based on the workload type:
I/O‑bound: 2 * n + 1 , where n is the number of CPU cores.
CPU‑bound: n + 1 .
Real‑world scenarios are often more complex. For framework‑level components like Netty or Dubbo, the above formulas are a good starting point.
In a practical example, a 4‑CPU, 8‑GB machine runs a RocketMQ consumer that uses a thread pool. Using the 2n+1 formula yields 9 threads, but experiments showed that increasing the thread count significantly improves message processing throughput, indicating that the simple formula may not fit the business case.
Another approach uses the formula threads = CPU cores / (1 - blockingFactor). With a blocking factor of 0.8, the calculation gives 20 threads; with 0.9 it yields about 40 threads. In practice, 20 threads proved reasonable for the workload.
If database operations dominate and become a bottleneck, the blocking factor can be increased to raise the thread count further.
To decide whether more threads are needed, you can inspect thread states with jstack. If most threads are waiting for tasks, the pool size is sufficient; if many are actively running, you may safely increase the pool size.
When most threads are in the RUNNABLE state, consider raising the thread count further.
That concludes this edition; hope it helps you, and feel free to give the author a quick three‑click encouragement.
Recommended reading:
Kafka Principles: Illustrated Architecture
Architecture Design Methodology
Kafka from an Interview Perspective
Database and Cache Dual‑Write Consistency
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Full-Stack Internet Architecture
Introducing full-stack Internet architecture technologies centered on Java
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
