- Understand the benefits of thread pools over manual thread creation
- Master different types of thread pools
- Learn to use Future for tracking task completion
- Know how to properly shut down executors
The Executor Framework
Creating threads manually with new Thread() works but doesn't scale. Each thread consumes system resources. Too many? Crash. Managing lifecycles manually? Error-prone. The Executor Framework solves this with a higher-level abstraction.
Why Use Executors?
Problems with Manual Thread Management
// Problematic: Creating a new thread for each task
for (int i = 0; i < 10000; i++) {
new Thread(() -> processRequest()).start();
}
// Issues:
// - 10,000 threads created (huge resource consumption)
// - No reuse of threads
// - No limit on concurrent threads
// - No orderly shutdown
Benefits of Executor Framework
| Aspect | Manual Threads | Executor Framework |
|---|---|---|
| Thread creation | Per task | Reused from pool |
| Resource management | Manual | Automatic |
| Task queuing | Not built-in | Built-in |
| Shutdown | Complex | Simple shutdown() |
| Returning results | Difficult | Built-in with Future |
The Executor Interface
The framework is based on a simple interface:
public interface Executor {
void execute(Runnable command);
}
You submit tasks; the executor decides when and how to run them.
ExecutorService
ExecutorService extends Executor with lifecycle management and result handling:
public interface ExecutorService extends Executor {
void shutdown(); // Graceful shutdown
List<Runnable> shutdownNow(); // Immediate shutdown
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit);
<T> Future<T> submit(Callable<T> task);
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
<T> T invokeAny(Collection<? extends Callable<T>> tasks);
}
Creating Thread Pools
The Executors utility class provides factory methods:
Fixed Thread Pool
A pool with a fixed number of threads:
ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit 100 tasks - only 4 run at a time
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " processing");
sleep(1000);
});
}
executor.shutdown();
Use for: CPU-bound tasks, when you want predictable resource usage.
Cached Thread Pool
Creates threads as needed, reuses idle threads:
ExecutorService executor = Executors.newCachedThreadPool();
// Creates threads on demand, up to Integer.MAX_VALUE
for (int i = 0; i < 100; i++) {
executor.execute(() -> {
System.out.println("Quick task on " + Thread.currentThread().getName());
});
}
executor.shutdown();
Use for: Many short-lived tasks, I/O-bound workloads.
Caution: Can create too many threads if tasks are slow!
Single Thread Executor
A single worker thread processing tasks sequentially:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(() -> System.out.println("Task 1"));
executor.execute(() -> System.out.println("Task 2"));
executor.execute(() -> System.out.println("Task 3"));
// Tasks execute in order: 1, 2, 3
executor.shutdown();
Use for: Tasks that must be serialized, background logging.
Scheduled Thread Pool
For delayed or periodic tasks:
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);
// Run after 5 seconds
scheduler.schedule(() -> System.out.println("Delayed task"),
5, TimeUnit.SECONDS);
// Run every 10 seconds
scheduler.scheduleAtFixedRate(() -> System.out.println("Periodic task"),
0, 10, TimeUnit.SECONDS);
// Run with 5-second delay between finish and start
scheduler.scheduleWithFixedDelay(() -> System.out.println("With delay"),
0, 5, TimeUnit.SECONDS);
Submitting Tasks
execute() vs submit()
// execute() - fire and forget
executor.execute(() -> System.out.println("Hello"));
// submit() - returns Future for tracking
Future<?> future = executor.submit(() -> System.out.println("Hello"));
future.get(); // Wait for completion
// submit() with result
Future<Integer> result = executor.submit(() -> {
return 42;
});
Integer value = result.get(); // Returns 42
Working with Future
Future represents a pending result:
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<String> future = executor.submit(() -> {
Thread.sleep(2000);
return "Task completed!";
});
// Do other work while task runs...
System.out.println("Doing other work...");
// Get result (blocks until available)
try {
String result = future.get(); // Waits up to forever
System.out.println(result);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
System.err.println("Task failed: " + e.getCause());
}
executor.shutdown();
Future Methods
| Method | Description |
|---|---|
get() |
Blocks until result available |
get(timeout, unit) |
Blocks with timeout |
isDone() |
Returns true if task completed |
isCancelled() |
Returns true if task was cancelled |
cancel(mayInterrupt) |
Attempts to cancel the task |
Timeout Example
Future<String> future = executor.submit(() -> {
Thread.sleep(10000); // Slow task
return "Done";
});
try {
String result = future.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("Task took too long!");
future.cancel(true); // Cancel the task
}
Submitting Multiple Tasks
invokeAll() - Wait for All
List<Callable<String>> tasks = Arrays.asList(
() -> { Thread.sleep(1000); return "Task 1"; },
() -> { Thread.sleep(2000); return "Task 2"; },
() -> { Thread.sleep(1500); return "Task 3"; }
);
// Waits for ALL tasks to complete
List<Future<String>> futures = executor.invokeAll(tasks);
for (Future<String> future : futures) {
System.out.println(future.get()); // Already complete
}
invokeAny() - First Result Wins
List<Callable<String>> tasks = Arrays.asList(
() -> { Thread.sleep(3000); return "Slow"; },
() -> { Thread.sleep(1000); return "Fast"; },
() -> { Thread.sleep(2000); return "Medium"; }
);
// Returns result of first completed task
String result = executor.invokeAny(tasks);
System.out.println(result); // Likely "Fast"
// Other tasks are cancelled
Proper Shutdown
Always shut down executors properly:
ExecutorService executor = Executors.newFixedThreadPool(4);
// Submit tasks...
executor.submit(() -> processData());
// Graceful shutdown
executor.shutdown(); // No new tasks accepted
try {
// Wait for existing tasks to complete
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
// Force shutdown if tasks don't complete in time
executor.shutdownNow();
// Wait again for tasks to respond to cancellation
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
System.err.println("Executor did not terminate");
}
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
Custom Thread Pool Configuration
For more control, use ThreadPoolExecutor directly:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // Core pool size
4, // Maximum pool size
60, TimeUnit.SECONDS, // Keep-alive time for excess threads
new ArrayBlockingQueue<>(100), // Work queue
new ThreadFactory() { // Custom thread factory
private int count = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "Worker-" + count++);
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // Rejection policy
);
Rejection Policies
When the queue is full and max threads are busy:
| Policy | Behavior |
|---|---|
AbortPolicy |
Throws RejectedExecutionException (default) |
CallerRunsPolicy |
Caller thread runs the task |
DiscardPolicy |
Silently discards the task |
DiscardOldestPolicy |
Discards oldest queued task |
Best Practices
- Always shutdown executors: Use try-finally or try-with-resources
- Size pools appropriately: CPU-bound → cores, I/O-bound → more threads
- Use bounded queues: Prevent memory exhaustion
- Handle exceptions: Tasks that throw exceptions fail silently
- Name your threads: Helps with debugging
- Avoid Executors.newCachedThreadPool() for unbounded work: Can create too many threads
The Executor Framework simplifies concurrent programming. Use thread pools instead of creating threads manually. Choose the right pool type for your workload. Always shut down executors properly. Use Future to track task completion and get results. invokeAll waits for all tasks, invokeAny returns first result. Configure ThreadPoolExecutor for fine-grained control.
