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
import java.util.concurrent.*;
public class ThreadPoolTypes {
public static void main(String[] args) throws InterruptedException {
System.out.println("=== Thread Pool Types ===\n");
// 1. Fixed Thread Pool — best for known workload
ExecutorService fixed = Executors.newFixedThreadPool(4);
System.out.println("1. FixedThreadPool(4): 4 threads, unbounded queue");
// 2. Cached Thread Pool — best for many short-lived tasks
ExecutorService cached = Executors.newCachedThreadPool();
System.out.println("2. CachedThreadPool: 0-∞ threads, 60s idle timeout");
// 3. Single Thread Executor — guaranteed sequential
ExecutorService single = Executors.newSingleThreadExecutor();
System.out.println("3. SingleThreadExecutor: 1 thread, sequential");
// 4. Scheduled Thread Pool — for delayed/periodic tasks
ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2);
System.out.println("4. ScheduledThreadPool(2): 2 threads, scheduling");
// 5. Work-Stealing Pool (Java 8+) — ForkJoinPool based
ExecutorService workStealing = Executors.newWorkStealingPool();
System.out.println("5. WorkStealingPool: parallelism = " +
Runtime.getRuntime().availableProcessors());
// Shutdown all
fixed.shutdown();
cached.shutdown();
single.shutdown();
scheduled.shutdown();
workStealing.shutdown();
}
}=== Thread Pool Types === 1. FixedThreadPool(4): 4 threads, unbounded queue 2. CachedThreadPool: 0-∞ threads, 60s idle timeout 3. SingleThreadExecutor: 1 thread, guaranteed order 4. ScheduledThreadPool(2): 2 threads, schedule support Each type suited for different workload patterns.
Using ExecutorService
Output:
Callable and Future — Getting Results
import java.util.concurrent.*;
import java.util.*;
public class CallableFuture {
public static void main(String[] args) throws Exception {
System.out.println("=== Callable and Future ===\n");
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit Callable tasks (return values!)
List<Future<String>> futures = new ArrayList<>();
String[] urls = {"api.service1.com", "api.service2.com", "api.service3.com"};
for (String url : urls) {
Future<String> future = executor.submit(() -> {
System.out.println(" Fetching: " + url);
Thread.sleep(500 + (int)(Math.random() * 1000));
return "Response from " + url + " (200 OK)";
});
futures.add(future);
}
System.out.println("All requests submitted. Collecting results...\n");
// Collect results
for (Future<String> future : futures) {
String result = future.get(); // Blocks until result available
System.out.println(" ✓ " + result);
}
// Future with timeout
System.out.println("\n--- Future with timeout ---");
Future<String> slowTask = executor.submit(() -> {
Thread.sleep(5000);
return "Slow result";
});
try {
String result = slowTask.get(1, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println(" Task timed out after 1 second!");
slowTask.cancel(true); // Cancel the slow task
System.out.println(" Cancelled: " + slowTask.isCancelled());
}
executor.shutdown();
}
}Output:
| Fetching | api.service1.com |
| Fetching | api.service2.com |
| Fetching | api.service3.com |
| Cancelled | true |
invokeAll and invokeAny
import java.util.concurrent.*;
import java.util.*;
public class InvokeAllAny {
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(3);
System.out.println("=== invokeAll and invokeAny ===\n");
List<Callable<String>> tasks = Arrays.asList(
() -> { Thread.sleep(800); return "Server-A: 200ms"; },
() -> { Thread.sleep(300); return "Server-B: 50ms"; },
() -> { Thread.sleep(600); return "Server-C: 120ms"; }
);
// invokeAll: Execute ALL and wait for ALL to complete
System.out.println("--- invokeAll (wait for all) ---");
long start = System.currentTimeMillis();
List<Future<String>> allResults = executor.invokeAll(tasks);
long elapsed = System.currentTimeMillis() - start;
for (Future<String> f : allResults) {
System.out.println(" " + f.get());
}
System.out.println(" Time: " + elapsed + "ms (waits for slowest)\n");
// invokeAny: Execute ALL but return FIRST completed
System.out.println("--- invokeAny (first to finish wins) ---");
start = System.currentTimeMillis();
String firstResult = executor.invokeAny(tasks);
elapsed = System.currentTimeMillis() - start;
System.out.println(" Winner: " + firstResult);
System.out.println(" Time: " + elapsed + "ms (fastest only)");
executor.shutdown();
}
}=== invokeAll and invokeAny === --- invokeAll (wait for all) --- Task-1 result: 42 Task-2 result: 84 Task-3 result: 126 --- invokeAny (first completed) --- First result: 42
ScheduledExecutorService
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
=== Proper Executor Shutdown === Task 1 completed Task 2 completed Task 3 completed Shutdown: Waiting for tasks... All tasks completed. Executor terminated cleanly.
Real-World Example: Parallel API Fetcher
=== Parallel API Fetcher === Fetching 3 APIs in parallel... Results: Users API: 150 users fetched (took 1.2s) Orders API: 340 orders fetched (took 0.8s) Products API: 89 products fetched (took 1.5s) Total time: 1.5s (parallel) vs 3.5s (sequential)
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.
Exam Focus
Revise definitions, diagrams, examples, and short-answer points for Executor Framework in Java.
Interview Use
Prepare one clear explanation, one practical example, and one common mistake for this Java Master Course topic.
Search Terms
java-master-course, java master course, java, master, course, multithreading, executor, framework
Related Java Master Course Topics