Executor Service and Future
ExecutorService is a Java framework that manages a pool of worker threads to execute your tasks asynchronously. Instead of manually creating and managing threads, you submit tasks to the executor and it handles all the threading complexity for you. At its core, an ExecutorService is a powerful framework that manages three key components:
[ Task Queue ] + [ Worker Threads ] + [ Lifecycle Manager ]
ThreadPoolExecutor
new ThreadPoolExecutor(
corePoolSize = 4,
maxPoolSize = 4,
keepAliveTime = 0,
workQueue = new LinkedBlockingQueue<>()
);
corePoolSize : The number of threads to keep alive even when idle. Think of this as your "baseline crew" that's always ready.
maxPoolSize : The maximum number of threads allowed in the pool. Extra threads (beyond core) are created when the queue fills up.
keepAliveTime How long excess threads (above core) stay alive when idle. After this time, they're terminated to free up resources.
workQueue The queue where tasks wait when all core threads are busy. Common types:
LinkedBlockingQueue- Unbounded queue (can grow infinitely)ArrayBlockingQueue- Bounded queue (fixed capacity)SynchronousQueue- No storage, direct handoff
Choosing the Right Thread Pool Size
For CPU-Bound Tasks (calculations, data processing)
Optimal threads = Number of CPU cores
int cores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(cores);
CPU-bound tasks keep cores busy.
More threads = more context switching = slower performance.
For I/O-Bound Tasks (database calls, HTTP requests, file operations)
Optimal threads = Number of cores × (1 + Wait time / Compute time)
// If tasks spend 90% time waiting and 10% computing
// Wait/Compute ratio = 9
int cores = Runtime.getRuntime().availableProcessors();
int threads = cores * (1 + 9); // cores × 10
ExecutorService executor = Executors.newFixedThreadPool(threads);
Optimal threads = Number of cores × (1 + Wait time / Compute time)
I/O tasks spend time waiting. While one thread waits, another can use the CPU.
Rule of thumb: 50-100 threads for typical I/O operations
Life of a Task
Future future = executor.submit(() -> {
return calculateTax(income);
});
Step 1: Task Wrapping
The task gets wrapped in a special object called FutureTask:

k
FutureTask implements both Runnable (for execution) and Future (for result retrieval). It's the bridge between execution and results.
Step 2: Task Submission Decision Tree

Step 3: Worker Thread Execution


Future
A Future is essentially a state machine that represents a computation that may not have completed yet.
Future States

It stores one of three things:
The result (when successful)
An exception (when failed)
A cancellation flag (when cancelled)
Why Future.get() Blocks ?
When we call future.get()
What happens internally:
The thread calling
get()parks itself usingLockSupport.park()It waits (sleeps) until the worker thread completes
Worker thread wakes it up when done
Result is returned
Example: Using Future
ExecutorService executor = Executors.newFixedThreadPool(2);
Future future = executor.submit(() -> {
Thread.sleep(2000); // Simulate work
return 42;
});
System.out.println("Task submitted, doing other work...");
// This blocks until result is ready
Integer result = future.get(); // Waits up to 2 seconds
System.out.println("Result: " + result);
executor.shutdown();
Cancelling Tasks
You can attempt to cancel a running task:
future.cancel(true); // true = interrupt if running
Important: Cancellation is cooperative. Your task must check for interruption:
while (!Thread.currentThread().isInterrupted()) {
// Do work
// Check periodically to respect cancellation
}
Runnable vs Callable

When to Use Runnable
// Logging, fire-and-forget operations
executor.execute(() -> {
log.info("User logged in: " + userId);
});
When to Use Callable
// Need the result
Future taxFuture = executor.submit(() -> {
return calculateTax(income);
});
Double tax = taxFuture.get();
ExecutorService Lifecycle
Proper shutdown is critical to prevent thread leaks and resource exhaustion.
❌ Wrong - Thread Leak!
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(task);
// Forgot to shutdown - threads never die!
✅ Correct - Graceful Shutdown
ExecutorService executor = Executors.newFixedThreadPool(4);
try {
executor.submit(task);
// ... more work
} finally {
executor.shutdown(); // No new tasks accepted
// Wait for tasks to complete
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // Force shutdown
}
}
Shutdown Methods
shutdown() - Graceful: no new tasks, finish existing ones
shutdownNow() - Forceful: interrupts running tasks, returns waiting tasks
awaitTermination() - Waits for shutdown to complete
Common Executor Types

Examples
// Fixed thread pool for CPU-bound work
ExecutorService cpuExecutor =
Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
// Cached thread pool for short bursts
ExecutorService burstExecutor =
Executors.newCachedThreadPool();
// Single thread for sequential processing
ExecutorService sequential =
Executors.newSingleThreadExecutor();
// Scheduled executor for periodic tasks
ScheduledExecutorService scheduler =
Executors.newScheduledThreadPool(2);
scheduler.scheduleAtFixedRate(
() -> System.out.println("Heartbeat"),
0, 5, TimeUnit.SECONDS
);
Completable Future
While Future is blocking, CompletableFuture provides a non-blocking, composable API for asynchronous programming.
Non-blocking: Chain operations without blocking threads
Composable: Combine multiple async operations
Exception handling: Built-in error handling with exceptionally()
Callbacks: Execute code when tasks complete
// Using Future (blocking):
Future future = executor.submit(() -> fetchUserName(id));
String userName = future.get(); // BLOCKS here
System.out.println(userName);
// Using CompletableFuture (non-blocking):
CompletableFuture.supplyAsync(() -> fetchUserName(id))
.thenApply(name -> name.toUpperCase())
.thenAccept(name -> System.out.println(name))
.exceptionally(ex -> {
System.err.println("Error: " + ex);
return null;
});
// No blocking - continues immediately!
Combining Multiple Tasks
CompletableFuture userFuture =
CompletableFuture.supplyAsync(() -> fetchUser(id));
CompletableFuture orderFuture =
CompletableFuture.supplyAsync(() -> fetchOrders(id));
// Wait for both to complete
CompletableFuture combined =
CompletableFuture.allOf(userFuture, orderFuture);
combined.thenRun(() -> {
String user = userFuture.join();
String orders = orderFuture.join();
System.out.println(user + " has orders: " + orders);
});
Common Pitfalls
1. Forgetting to Shutdown
Always shutdown executors in a finally block or use try-with-resources (Java 19+)
2. Deadlocks with Future.get()
// DEADLOCK! Only 1 thread, but task waits for itself
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(() -> {
Future f = executor.submit(() -> "Hello");
return f.get(); // Waits forever!
});
3. Wrong Pool Size
Using CPU core count for I/O tasks = underutilisation
Using too many threads for CPU tasks = excessive context switching
4. Sharing State Without Synchronisation
ExecutorService doesn't make your code thread-safe. You still need proper synchronisation.