Java Executors

🤔 Today I learned something interesting about Java Executors…

Most of us use Java Executors because they are easy to use… But easy doesn’t always mean correct

Article content

Java Executors are convenient, but they don’t give full control over threads, queue, and scaling.

👉 That’s why using a custom ThreadPoolExecutor is often a better choice.


Recommended Code:

int corePoolSize = 0;
int maxPoolSize = 20;
long keepAliveTime = 60L;

BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(20);

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    corePoolSize,
    maxPoolSize,
    keepAliveTime,
    TimeUnit.SECONDS,
    queue
);

executor.allowCoreThreadTimeOut(true);
        

🔍 Explanation:

corePoolSize = 0 → No threads initially, threads are created on demand

maximumPoolSize = 20 → Maximum 20 threads can run

keepAliveTime = 60s → Idle threads are removed after 60 seconds

allowCoreThreadTimeOut(true) → Even core threads can be removed


👉 In this configuration, since the queue size is 20 (bounded queue) and corePoolSize = 0, tasks are first added to the queue instead of creating multiple threads immediately(Since corePoolSize is 0 therefore 1 thread will be created otherwise equal to corePoolSize). Initially, only one thread is created to start processing tasks. As long as the queue is not full, new tasks will keep getting added to the queue and no additional threads will be created. Once the queue becomes full, new threads will start getting created (up to maxPoolSize) to handle incoming tasks. If tasks come very fast and exceed the queue capacity (20), they will first fill the queue, and then additional threads will be used.

Now here is an important point 👇

If you use corePoolSize = 0 + unbounded queue, then even if maxPoolSize is 100, mostly only one thread will keep executing tasks.

👉 It behaves almost like a single-threaded system and becomes very slow.


So, if you want to actually use maxPoolSize, you must use a bounded queue.

Because new threads are created only when the queue becomes full.

Otherwise, tasks stay in the queue and threads don’t grow.


If you don’t want too many tasks in memory, use a bounded queue along with a rejection policy.


Now if you are using a bounded queue, then remember:

If tasks coming at the same time exceed (maxPoolSize + queue capacity),

💥 RejectedExecutionException will occur

Because all threads are busy and the queue is also full.

👉 A task is rejected only when both the thread pool and the queue are full at the same time.


🚀 Solution: Use a task rejection policy

executor.setRejectedExecutionHandler(
    new ThreadPoolExecutor.CallerRunsPolicy()
);
        

This will not drop the task. The calling thread will execute it. The system may slow down, but it will not crash.


🧠 Final Conclusion:

Avoid unbounded queues in most cases (memory risk + no scaling)

Use bounded queues for better control

Always handle task rejection properly

👉 Most importantly: Choose the executor based on your requirement, not just because it is simple to use.


#Java #Multithreading #Concurrency #ThreadPool

To view or add a comment, sign in

Others also viewed

Explore content categories