Why Using Executors to Build Thread Pools Can Hurt Your Java Apps—and How to Do It Right
This article explains the pitfalls of creating Java thread pools with Executors, details how to correctly build them using ThreadPoolExecutor, clarifies core parameters, queue strategies, and rejection policies, and provides runnable code examples to illustrate proper usage.
Drawbacks of Creating Thread Pools with Executors
Most developers use Executors to create thread pools, but this practice violates Alibaba's coding guidelines and can lead to resource exhaustion.
Issues include:
newFixedThreadPool and newSingleThreadExecutor : the unbounded request queue may consume massive memory and cause OOM.
newCachedThreadPool and newScheduledThreadPool : they can create up to Integer.MAX_VALUE threads, also risking OOM.
Creating Thread Pools with ThreadPoolExecutor
Replace the non‑standard code with a direct ThreadPoolExecutor construction:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
} ThreadPoolExecutoris the core implementation of a thread pool, reducing thread creation overhead and providing built‑in statistics for monitoring.
ThreadPoolExecutor Parameter Explanation
corePoolSize & maximumPoolSize
When a task is submitted:
If the running thread count is less than corePoolSize, a new thread is created.
If the count is between corePoolSize and maximumPoolSize, a new thread is created only when the queue is full.
If the count exceeds maximumPoolSize, the task is handled by the rejection policy.
keepAliveTime & unit
Specifies the maximum idle time for threads that exceed corePoolSize, with unit defining the time unit.
Waiting Queue
Any BlockingQueue can be used, and its size interacts with the pool size:
If running threads < corePoolSize, a new thread is created for each task.
If running threads ≥ corePoolSize, tasks are queued; when the queue is full and threads < maximumPoolSize, new threads are created.
If threads > maximumPoolSize, the rejection policy is applied.
Common Queue Strategies
Direct handoff (SynchronousQueue): tasks are handed directly to a thread; if none is available, a new thread is created.
Unbounded queue (LinkedBlockingQueue): tasks wait in an unbounded queue, preventing creation of threads beyond corePoolSize.
Bounded queue (ArrayBlockingQueue): limits queue size, helping avoid resource exhaustion but making tuning more complex.
Rejection Policies
When the pool is shut down or saturated, four built‑in policies are available:
AbortPolicy : throws RejectedExecutionException.
CallerRunsPolicy : runs the task in the calling thread.
DiscardPolicy : silently discards the task.
DiscardOldestPolicy : discards the oldest queued task and retries the new one.
Custom policies can be implemented by providing a RejectedExecutionHandler.
ThreadPoolExecutor Creation Demo
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadPoolSerialTest {
public static void main(String[] args) {
int corePoolSize = 3;
int maximumPoolSize = 6;
long keepAliveTime = 2;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<Runnable>(2);
ThreadPoolExecutor threadPoolExecutor = null;
try {
threadPoolExecutor = new ThreadPoolExecutor(corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
new ThreadPoolExecutor.AbortPolicy());
for (int i = 0; i < 8; i++) {
final int index = i + 1;
threadPoolExecutor.submit(() -> {
System.out.println("Hello, I am thread: " + index);
try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); }
});
Thread.sleep(500);
}
} catch (InterruptedException e) { e.printStackTrace(); }
finally { threadPoolExecutor.shutdown(); }
}
}The execution flow:
ThreadPoolExecutor is instantiated.
Eight tasks are submitted (corePoolSize = 3, queue capacity = 2, maximumPoolSize = 6).
First three tasks create core threads; the next two are queued; the sixth task creates an extra thread; the seventh and eighth create threads up to the maximum.
When core threads finish, they pick queued tasks, avoiding frequent thread creation and destruction.
Demonstrating Rejection Policies
Submitting nine tasks (exceeding maximumPoolSize + queue capacity) triggers the policies. The following images illustrate each policy's behavior:
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
