Java Notes
Complete guide to the Executor Framework in Java — ExecutorService, thread pools, Future, Callable, ScheduledExecutorService, and modern concurrent programming patterns.
Why Executors Instead of Manual Threads?
Creating threads manually has problems:
- Thread creation is expensive (OS resources)
- Too many threads → memory exhaustion, context-switch overhead
- No built-in task queuing or result handling
- Manual lifecycle management is error-prone
The Executor Framework (Java 5+) provides:
- Thread pooling (reuse threads)
- Task submission and queuing
- Future results from async tasks
- Scheduled execution
- Graceful shutdown
The Executor Hierarchy
Creating Thread Pools
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Using ExecutorService
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Output:
Callable and Future — Getting Results
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Output:
| Fetching | api.service1.com |
| Fetching | api.service2.com |
| Fetching | api.service3.com |
| Cancelled | true |
invokeAll and invokeAny
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
ScheduledExecutorService
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Output:
| [14 | 30:45.123] Periodic tick |
| [14 | 30:46.125] Periodic tick |
| [14 | 30:47.123] Delayed task executed! |
| [14 | 30:47.124] Periodic tick |
| [14 | 30:48.125] Periodic tick |
Proper Shutdown Pattern
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Real-World Example: Parallel API Fetcher
Try it in WoHoCode
Edit the program, then run it here.
Run this example to see its output.
Common Mistakes
1. Not shutting down the executor
// ❌ BAD: Program never exits (non-daemon threads in pool)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(task);
// Forgot to shutdown! JVM keeps running.
// ✅ GOOD: Always shutdown
executor.shutdown();2. Using unbounded pools for I/O tasks
3. Ignoring Future exceptions
// ❌ BAD: Exception is silently swallowed
executor.submit(() -> { throw new RuntimeException("Error!"); });
// ✅ GOOD: Check the Future
Future<?> future = executor.submit(() -> { throw new RuntimeException("Error!"); });
try {
future.get(); // Throws ExecutionException wrapping the RuntimeException
} catch (ExecutionException e) {
System.err.println("Task failed: " + e.getCause().getMessage());
}Interview Questions
Q1: What is a thread pool and why use one?
Answer: A thread pool is a collection of pre-created threads that are reused to execute tasks. Benefits: (1) avoids thread creation overhead, (2) limits concurrent threads to prevent resource exhaustion, (3) provides task queuing, (4) enables result handling via Future.
Q2: What is the difference between execute() and submit()?
Answer: execute(Runnable) is void — fire and forget, exceptions are lost. submit(Runnable/Callable) returns a Future — you can check completion, get results, handle exceptions, and cancel tasks.
Q3: What is the difference between shutdown() and shutdownNow()?
Answer: shutdown(): stops accepting new tasks, lets queued/running tasks finish. shutdownNow(): stops accepting new tasks, interrupts running tasks, returns list of queued tasks that never started. Use shutdown() normally; shutdownNow() for forced termination.
Q4: What happens when all threads in a fixed pool are busy?
Answer: New submitted tasks are placed in a work queue (LinkedBlockingQueue by default, which is unbounded). Tasks wait in the queue until a thread becomes available. If the queue fills up (for bounded queues), a RejectedExecutionHandler is invoked.
Q5: When should you use CachedThreadPool vs FixedThreadPool?
Answer: CachedThreadPool: many short-lived tasks where thread count is unpredictable (creates threads as needed, reclaims idle ones). FixedThreadPool: predictable workload where you want to limit concurrency (web server handling requests). Avoid CachedThreadPool for long-running tasks — it can create too many threads.
Q6: What is Future.get() and what happens if the task throws an exception?
Answer: Future.get() blocks until the task completes and returns the result. If the task threw an exception, get() throws ExecutionException wrapping the original exception (accessible via getCause()). You can also use get(timeout, unit) to limit waiting time.
Q8: What happens if a task throws an exception in ExecutorService?
A: For submit(): the exception is captured in the Future. Calling future.get() throws ExecutionException wrapping the original exception. The thread pool thread doesn't die — it returns to the pool. For execute(): the thread dies and the uncaught exception handler is called. A new thread replaces it in the pool.
Q9: What is the difference between shutdown() and shutdownNow()?
A: shutdown(): gracefully stops — no new tasks accepted, but already submitted tasks complete. shutdownNow(): aggressive — attempts to cancel running tasks (via interrupt), returns list of unexecuted tasks. Use shutdown() normally; shutdownNow() for emergency cleanup. Always call one before application exit.
Q10: How do you choose the right thread pool size?
A: For CPU-bound tasks: pool size = number of CPU cores (Runtime.getRuntime().availableProcessors()). For I/O-bound tasks: larger pool (cores × 2 or more) since threads spend time waiting. The formula: threads = cores × (1 + waitTime/computeTime). Profile your application to find the optimal size.
Q11: What is the difference between FixedThreadPool and CachedThreadPool?
A: FixedThreadPool: fixed number of threads, tasks queue if all busy, predictable resource usage. CachedThreadPool: creates new threads on demand, reuses idle threads (60s timeout), can grow unbounded. Use Fixed for predictable load; use Cached for many short-lived tasks. Avoid Cached with unbounded task submission — can OOM.
Q12: What is CompletableFuture and how does it improve on Future?
A: CompletableFuture (Java 8+) extends Future with: (1) non-blocking callbacks — thenApply(), thenAccept(), thenCompose(). (2) Exception handling — exceptionally(), handle(). (3) Combining futures — thenCombine(), allOf(), anyOf(). Regular Future only supports blocking get(). CompletableFuture enables reactive-style async programming.
Continue the lesson path