-
ExecutorServiceis an interface in Java from thejava.util.concurrentpackage. -
It provides a way to manage and control multiple threads β including:
- Submitting tasks to be executed
- Managing their lifecycle
- Shutting them down properly
-
Executorsis a utility class that provides factory methods to create different kinds of thread pools. -
newFixedThreadPool(5)creates a thread pool with a fixed number (5) of threads. -
This means:
- At most 5 threads will be running concurrently.
- If more than 5 tasks are submitted, the extra tasks will wait in a queue until a thread is free.
ExecutorService executorService = Executors.newFixedThreadPool(5);-
It creates a thread pool of 5 threads.
-
You can now use
executorServiceto submit tasks like this:executorService.submit(() -> { // some background task System.out.println("Task is running in a thread"); });
-
It helps reuse threads and improves performance by avoiding the cost of creating new threads for every task.
Imagine you want to send 100 emails:
- Instead of creating 100 threads, you can use a fixed thread pool of 5 threads.
- It will send emails in parallel (5 at a time), improving performance and saving system resources.
Once you're done submitting tasks, shut it down properly:
executorService.shutdown();| Component | Explanation |
|---|---|
ExecutorService |
Interface to manage threads and tasks |
Executors.newFixedThreadPool(5) |
Creates a pool with 5 threads |
| Purpose | Efficiently manage multiple concurrent tasks with a limited number of threads |
You can create threads manually in Java using new Thread(...). But using ExecutorService has many advantages, especially when working on real-world, scalable applications.
Letβs compare manual thread creation vs. ExecutorService, and explain why ExecutorService is preferred:
Thread t = new Thread(() -> {
// do some work
});
t.start();- Simple for one or two tasks.
- Gives you full control over thread creation.
- No thread reuse: Each
Threadis created fresh and destroyed after the task ends β slow and memory-heavy. - No task queue: You must manage your own queue if tasks arrive faster than threads can handle.
- No thread management: You can't easily manage a pool of threads or control how many run concurrently.
- No built-in error handling for task rejection or timeouts.
- Hard to scale: If you have hundreds or thousands of tasks, manual thread creation becomes a mess.
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.submit(() -> {
// do some work
});- Thread pooling: Threads are reused β no overhead of constant creation/destruction.
- Automatic task queueing: Extra tasks wait in a queue.
- Better resource management: Limits how many threads run concurrently (e.g., to avoid CPU overload).
- Scalable: Easily handles many tasks.
- Graceful shutdown: You can
shutdown()orawaitTermination()to properly manage application lifecycle. - Future support: You can use
Futureto get results or handle exceptions from tasks. - Scheduled tasks: You can schedule tasks with delays or periodically using
ScheduledExecutorService.
| Use Case | Recommended Approach |
|---|---|
| A few quick tasks | Thread or Runnable is okay |
| Many tasks or long-running services | ExecutorService is better |
| You need result/failure of task | ExecutorService with Future |
| You need control over threads | Use a custom ExecutorService or a ThreadPoolExecutor |
While manual thread creation is fine for simple cases, ExecutorService gives you a production-ready, scalable, and robust way to manage concurrent tasks. It's widely used in web servers, background workers, scheduled tasks, etc.
The reason Java provides so many types of Executors is that different use cases need different threading strategies. There is no one-size-fits-all. Instead, Java gives you a toolbox β you pick the right one based on your needs.
| Executor | Thread Strategy | When to Use |
|---|---|---|
newFixedThreadPool(int n) |
Fixed number of reusable threads | You have many short tasks and want to limit concurrency |
newSingleThreadExecutor() |
Only one thread | You want to run tasks one at a time, in order |
newCachedThreadPool() |
Unlimited threads, reuses idle threads | You have many short-lived tasks, possibly bursty |
newScheduledThreadPool(int n) |
Fixed threads + supports delays | You need to schedule tasks (like a cron job or timer) |
newSingleThreadScheduledExecutor() |
One thread + scheduling support | Same as above, but for sequential scheduled tasks |
newWorkStealingPool() |
ForkJoinPool with multiple queues | Best for many small parallel tasks (CPU-bound work) |
newThreadPerTaskExecutor(ThreadFactory) |
Creates new thread for each task | Avoid unless you want a thread per task (heavyweight) |
newVirtualThreadPerTaskExecutor() (Java 21+) |
New virtual thread per task | Great for high concurrency, like servers, light threads |
Letβs map them to real-world scenarios:
β Best for most common use cases
- Reuse a fixed number of threads
- CPU-bound or IO-bound tasks
- You want to limit the number of concurrent threads
π§ Use this when:
ExecutorService pool = Executors.newFixedThreadPool(10);E.g., 10 threads to process user uploads.
β Ensures sequential execution
- Tasks are queued and run one after another
- Used when order matters
π§ E.g., logging, file-writing in order.
β Dynamic scaling, unlimited threads
- Great for bursty traffic, short-lived tasks
- Idle threads are reused
β Risk: Can exhaust system resources if many long tasks pile up
π§ E.g., Fire-and-forget tasks like email sending.
β Use for repeated/scheduled tasks
- Supports
.schedule()and.scheduleAtFixedRate()
π§ E.g., run DB cleanup every hour.
β Like above, but with just 1 thread
π§ E.g., Schedule one task to run hourly in strict order
β Uses a ForkJoinPool, good for divide-and-conquer
- Best for parallel computation
- Tasks are split and stolen by idle threads
π§ E.g., recursive algorithms, sorting, parallel tasks
β Not commonly used β creates one thread per task
- Very heavy if tasks are frequent
- Used rarely (mostly for experimentation)
π₯ Next-gen concurrency: lightweight threads (Project Loom)
- Thousands of virtual threads can run on few platform threads
- Great for high-concurrency I/O-heavy apps
π§ E.g., modern server handling thousands of connections.
| Need | Recommended |
|---|---|
| 1 task at a time | newSingleThreadExecutor() |
| Many tasks, limited concurrency | newFixedThreadPool(n) |
| Short tasks, unpredictable load | newCachedThreadPool() |
| Scheduled tasks | newScheduledThreadPool() |
| CPU-intensive, divide-and-conquer | newWorkStealingPool() |
| Modern Java, high concurrency | newVirtualThreadPerTaskExecutor() (Java 21+) |
The order of task execution in an ExecutorService depends on which type of executor you use.
Letβs break it down by executor type:
- Tasks run in the order you submit them (FIFO β First In, First Out).
- There is only one thread, so they can't run in parallel.
π§ Guaranteed order.
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));
executor.submit(() -> System.out.println("Task 3"));
// Output: Task 1 β Task 2 β Task 3- No guaranteed order.
- Tasks are taken from a queue, but multiple threads pick them up, so the execution can be out of order.
π§ Submission order β execution order, especially if:
- Tasks take different times.
- Threads pick up tasks in a different order.
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task 1"));
executor.submit(() -> System.out.println("Task 2"));
executor.submit(() -> System.out.println("Task 3"));
// Possible output: Task 2 β Task 1 β Task 3- Similar to
FixedThreadPoolβ no guaranteed order. - Spawns new threads as needed, so task execution order can vary a lot.
- If you schedule tasks with delays, execution depends on time, not submission order.
- Uses multiple queues per thread, so task order is not guaranteed at all.
- Focus is on throughput, not order.
- Creates a new thread per task β no order guarantee.
- Tasks run concurrently.
| Executor Type | Execution Order |
|---|---|
SingleThreadExecutor |
β In order |
FixedThreadPool |
β Not guaranteed |
CachedThreadPool |
β Not guaranteed |
ScheduledThreadPool |
β° Based on delay/period |
WorkStealingPool |
β Not guaranteed |
VirtualThreadPerTaskExecutor |
β Not guaranteed |
ThreadPerTaskExecutor |
β Not guaranteed |
β If order matters, go with:
SingleThreadExecutor(for serial tasks)- Or use a blocking queue outside the pool and control the order manually.
When you submit a task to an ExecutorService, it doesnβt return the result of the task directly β it returns a Future object instead.
A Future<T> represents the result of an asynchronous computation.
It acts like a promise that:
- The task will complete
- You can check if it's done
- You can get the result when ready
- You can cancel the task
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(() -> {
Thread.sleep(2000); // Simulate work
return 42;
});Now:
System.out.println("Task submitted...");
Integer result = future.get(); // This will block until result is ready
System.out.println("Result = " + result);| Method | What it does |
|---|---|
get() |
Blocks and waits for the result |
get(timeout, unit) |
Waits for result for given time, else throws TimeoutException |
isDone() |
Returns true if task is finished |
isCancelled() |
Returns true if task was cancelled |
cancel(true) |
Attempts to cancel the task |
Future<String> future = executor.submit(() -> {
Thread.sleep(3000);
return "Hello from background!";
});
System.out.println("Doing other stuff...");
String result = future.get(); // Waits here if not done
System.out.println(result);You can use execute() instead of submit():
executor.execute(() -> System.out.println("Fire and forget task"));But then:
- You donβt get result
- You canβt know if it succeeded or failed
- You canβt cancel it
Use it when:
- You want to get the result
- You want to check or control task execution
- You want to handle exceptions
Future is basic. For chaining, async workflows, etc., prefer CompletableFuture.
CompletableFuture<T> is a more powerful and flexible version of Future introduced in Java 8.
It allows you to:
- Run tasks asynchronously
- Chain tasks together
- Handle results, errors, and exceptions
- Write non-blocking code
- Easily use functional programming
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
// Runs in a background thread
return "Hello";
});To get the result:
String result = future.get(); // Blocks until result is readyOr handle it without blocking:
future.thenAccept(result -> System.out.println("Result: " + result));CompletableFuture.supplyAsync(() -> "Hello")
.thenApply(str -> str + " World")
.thenAccept(System.out::println); // Prints: Hello WorldEach stage runs only after the previous one completes.
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "A");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "B");
f1.thenCombine(f2, (a, b) -> a + b)
.thenAccept(System.out::println); // Prints: ABCompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("Boom");
return "Hello";
}).exceptionally(ex -> {
System.out.println("Caught: " + ex.getMessage());
return "Fallback";
});| Feature | Future |
CompletableFuture |
|---|---|---|
| Introduced in | Java 5 | Java 8 |
| Result retrieval | get() (blocks) |
get(), or non-blocking with then...() |
| Async execution | Needs ExecutorService |
Built-in support via supplyAsync(), etc. |
| Task chaining | β Not possible | β
Fluent chaining with thenApply, etc. |
| Combining multiple tasks | β No built-in support | β
Easy with thenCombine, allOf, etc. |
| Error handling | Manual via try-catch | Built-in: exceptionally(), handle() |
| Non-blocking support | β Only blocking with get() |
β Full support for non-blocking |
| Event-based callbacks | β Not available | β Supported |
| Completing manually | β No | β
Can call complete(), completeExceptionally() |
-
β Use
Futurefor simple async tasks where you just need:- To submit
- To wait for result
- Maybe cancel it
-
β Use
CompletableFuturefor:- Complex workflows
- Chaining and combining tasks
- Parallel execution
- Better error handling
- Non-blocking applications (e.g., web services)
Sure! Here's a complete example for each use case using CompletableFuture. These patterns are commonly used in real-world backend services like microservices or async APIs.
CompletableFuture<String> api1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "API 1 result";
});
CompletableFuture<String> api2 = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "API 2 result";
});
CompletableFuture<String> combined = api1.thenCombine(api2, (res1, res2) -> res1 + " + " + res2);
System.out.println(combined.get()); // Output after 2 seconds: API 1 result + API 2 resultSince CompletableFuture has no direct retry method, we write a wrapper:
public static <T> CompletableFuture<T> retry(Supplier<T> task, int attempts) {
return CompletableFuture.supplyAsync(() -> {
while (true) {
try {
return task.get();
} catch (Exception e) {
if (--attempts == 0) throw new RuntimeException("All retries failed", e);
System.out.println("Retrying...");
sleep(500);
}
}
});
}
// Usage:
CompletableFuture<String> retried = retry(() -> {
if (Math.random() < 0.7) throw new RuntimeException("API failed");
return "Success";
}, 3);
System.out.println(retried.get());CompletableFuture<String> slowApi = CompletableFuture.supplyAsync(() -> {
sleep(3000);
return "Slow API result";
});
// Timeout logic
CompletableFuture<String> withTimeout = slowApi
.completeOnTimeout("Fallback value", 2, TimeUnit.SECONDS);
System.out.println(withTimeout.get()); // Will print "Fallback value" if API takes >2sprivate static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}Let's explore how to schedule tasks at a fixed rate or with a delay using ScheduledExecutorService.
This is useful when you want to:
- Run a task repeatedly at regular intervals
- Delay the execution of a task
- Run periodic background jobs like health checks, log cleanup, etc.
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);scheduler.scheduleWithFixedDelay(() -> {
System.out.println("Task with delay - " + System.currentTimeMillis());
sleep(1000); // simulate work
}, 0, 2, TimeUnit.SECONDS);- Starts immediately (0s delay)
- Waits 2 seconds after the task finishes before starting again
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Fixed rate task - " + System.currentTimeMillis());
}, 0, 3, TimeUnit.SECONDS);- Starts immediately
- Then runs every 3 seconds, no matter how long the task takes
- If the task takes longer than 3s, it tries to catch up
scheduler.schedule(() -> {
System.out.println("Run after 5 seconds delay");
}, 5, TimeUnit.SECONDS);ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(() -> {
System.out.println("Running task...");
}, 0, 2, TimeUnit.SECONDS);
// Cancel after 10 seconds
scheduler.schedule(() -> {
System.out.println("Cancelling...");
future.cancel(true);
scheduler.shutdown();
}, 10, TimeUnit.SECONDS);| Feature | scheduleAtFixedRate |
scheduleWithFixedDelay |
|---|---|---|
| Time between executions | Fixed, regardless of task duration | Waits a fixed delay after task finishes |
| Use when | You want precise periodic execution | You want fixed gap between runs |
| Risk | Tasks may overlap or pile up if too slow | Safer β always waits before next run |
letβs go deeper into cron-like scheduling and real-world examples using ScheduledExecutorService.
Java itself does not have a built-in cron parser like Linux cron, but you can:
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Runs every 10 seconds like a cron job");
}, 0, 10, TimeUnit.SECONDS);This is like a cron: */10 * * * * * (every 10 seconds)
<!-- Add to pom.xml -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("cronJob")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
.build();
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
scheduler.scheduleJob(job, trigger);Where MyJob is:
public class MyJob implements Job {
public void execute(JobExecutionContext context) {
System.out.println("Quartz Cron Job Running at " + new Date());
}
}β Use this when you need real cron expressions like:
0 0/15 * * * ?β every 15 minutes0 0 9 ? * MON-FRIβ 9 AM every weekday
scheduler.scheduleAtFixedRate(() -> {
System.out.println("Checking DB for new records at " + new Date());
// Simulate DB poll
List<String> newRecords = fetchNewRecordsFromDB();
newRecords.forEach(System.out::println);
}, 0, 10, TimeUnit.SECONDS);private static List<String> fetchNewRecordsFromDB() {
// Simulated data
return List.of("Record 1", "Record 2");
}Suppose we retry failed messages with backoff:
public void retryWithBackoff(String message, int attempt) {
long delay = (long) Math.pow(2, attempt); // 2^attempt seconds
scheduler.schedule(() -> {
try {
sendToApi(message);
} catch (Exception e) {
if (attempt < 5) {
retryWithBackoff(message, attempt + 1);
} else {
System.out.println("Max retries reached. Dropping message.");
}
}
}, delay, TimeUnit.SECONDS);
}public void sendToApi(String msg) {
System.out.println("Sending: " + msg);
if (Math.random() < 0.7) throw new RuntimeException("Failed!");
System.out.println("Success: " + msg);
}| Goal | Best Approach |
|---|---|
| Cron-like precision | Use Quartz or Spring's @Scheduled(cron) |
| Polling DB/API | Use scheduleAtFixedRate |
| Retry with backoff | Use recursive scheduler.schedule() |
| Lightweight jobs | Use ScheduledExecutorService |