Executors in Java
Learn how to manage thread pools and execute tasks efficiently with the Executor framework
Think of Executors like a company's HR department managing workers! Instead of hiring and firing a new employee for every single task (creating and destroying threads), you have a team of workers (thread pool) who are always ready. When a new task comes in, you assign it to an available worker. This is much more efficient than creating a new thread every time, just like it's better to have a team ready than hiring someone new for each task!
The Problem Without Executors
Creating a new thread for every task is expensive and wasteful! Threads take time and memory to create. If you have thousands of tasks, creating thousands of threads will crash your program. Let's see this problem:
// Without Executors - inefficient thread managementpublic class WithoutExecutorExample { public static void main(String[] args) { System.out.println("Processing 10 tasks without executor..."); long startTime = System.currentTimeMillis(); // Creating a new thread for each task - wasteful! for (int i = 1; i <= 10; i++) { final int taskId = i; // Each task gets its own new thread Thread thread = new Thread(() -> { System.out.println("Task " + taskId + " started by " + Thread.currentThread().getName()); try { // Simulate some work Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " completed"); }); thread.start(); // Problem: Need to manually manage all threads try { thread.join(); // Wait for this thread to finish } catch (InterruptedException e) { e.printStackTrace(); } } long endTime = System.currentTimeMillis(); System.out.println("\nTotal time: " + (endTime - startTime) + "ms"); System.out.println("Created 10 threads (expensive!)"); }}/* Output:Processing 10 tasks without executor...Task 1 started by Thread-0Task 1 completedTask 2 started by Thread-1Task 2 completedTask 3 started by Thread-2Task 3 completed...Task 10 completedTotal time: ~10000msCreated 10 threads (expensive!)PROBLEMS:1. Created 10 threads - lots of memory overhead2. Tasks run sequentially because we wait for each3. Manual thread management is error-prone4. No reuse - threads are destroyed after one task*/Fixed Thread Pool - Basic Executor Usage
ExecutorService with a fixed thread pool is like having a permanent team of workers! You create a pool of threads once, and they handle all your tasks. When a task is done, the worker doesn't retire - they're ready for the next task. Much more efficient!
// Using ExecutorService with fixed thread poolimport java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;public class FixedThreadPoolExample { public static void main(String[] args) { System.out.println("Processing 10 tasks with executor (3 workers)..."); long startTime = System.currentTimeMillis(); // Create a pool of 3 worker threads ExecutorService executor = Executors.newFixedThreadPool(3); // Submit 10 tasks to the executor for (int i = 1; i <= 10; i++) { final int taskId = i; executor.submit(() -> { System.out.println("Task " + taskId + " started by " + Thread.currentThread().getName()); try { Thread.sleep(1000); // Simulate work } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Task " + taskId + " completed by " + Thread.currentThread().getName()); }); } // Shutdown the executor (no more tasks will be accepted) executor.shutdown(); try { // Wait for all tasks to complete (max 20 seconds) if (executor.awaitTermination(20, TimeUnit.SECONDS)) { System.out.println("\nAll tasks completed!"); } else { System.out.println("\nTimeout! Some tasks still running."); } } catch (InterruptedException e) { e.printStackTrace(); } long endTime = System.currentTimeMillis(); System.out.println("Total time: " + (endTime - startTime) + "ms"); System.out.println("Only created 3 threads (reused for all tasks!)"); }}/* Output:Processing 10 tasks with executor (3 workers)...Task 1 started by pool-1-thread-1Task 2 started by pool-1-thread-2Task 3 started by pool-1-thread-3Task 1 completed by pool-1-thread-1Task 4 started by pool-1-thread-1Task 2 completed by pool-1-thread-2Task 5 started by pool-1-thread-2Task 3 completed by pool-1-thread-3Task 6 started by pool-1-thread-3...All tasks completed!Total time: ~4000msOnly created 3 threads (reused for all tasks!)BENEFITS:1. Only 3 threads created (saves memory!)2. Threads are reused for multiple tasks3. Automatic task queue management4. Much faster - tasks run in parallel*/Different Types of Executors
Java provides different types of executor services for different needs! Like different types of teams: a fixed team, a flexible team that grows/shrinks, a single worker, or a scheduled team for recurring tasks. Choose the right one for your situation!
// Exploring different types of Executorsimport java.util.concurrent.*;public class ExecutorTypesExample { public static void main(String[] args) throws InterruptedException { System.out.println("=== 1. Fixed Thread Pool ==="); // Always has exactly 2 threads ExecutorService fixed = Executors.newFixedThreadPool(2); submitTasks(fixed, "Fixed", 4); fixed.shutdown(); fixed.awaitTermination(5, TimeUnit.SECONDS); System.out.println("\n=== 2. Cached Thread Pool ==="); // Creates threads as needed, reuses idle ones // Good for many short-lived tasks ExecutorService cached = Executors.newCachedThreadPool(); submitTasks(cached, "Cached", 4); cached.shutdown(); cached.awaitTermination(5, TimeUnit.SECONDS); System.out.println("\n=== 3. Single Thread Executor ==="); // Only 1 thread - tasks run sequentially // Good for tasks that must run in order ExecutorService single = Executors.newSingleThreadExecutor(); submitTasks(single, "Single", 3); single.shutdown(); single.awaitTermination(5, TimeUnit.SECONDS); System.out.println("\n=== 4. Scheduled Thread Pool ==="); // Can schedule tasks to run after delay or periodically ScheduledExecutorService scheduled = Executors.newScheduledThreadPool(2); System.out.println("Scheduling task to run after 1 second..."); scheduled.schedule(() -> { System.out.println("Delayed task executed!"); }, 1, TimeUnit.SECONDS); System.out.println("Scheduling repeating task (every 2 seconds)..."); ScheduledFuture<?> repeating = scheduled.scheduleAtFixedRate(() -> { System.out.println("Repeating task tick at " + System.currentTimeMillis()); }, 0, 2, TimeUnit.SECONDS); // Let it run for 6 seconds Thread.sleep(6000); repeating.cancel(true); // Stop the repeating task scheduled.shutdown(); scheduled.awaitTermination(2, TimeUnit.SECONDS); System.out.println("\nAll executor demos complete!"); } static void submitTasks(ExecutorService executor, String name, int count) { for (int i = 1; i <= count; i++) { final int taskId = i; executor.submit(() -> { System.out.println(name + " - Task " + taskId + " by " + Thread.currentThread().getName()); try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } }); } }}/* Output:=== 1. Fixed Thread Pool ===Fixed - Task 1 by pool-1-thread-1Fixed - Task 2 by pool-1-thread-2Fixed - Task 3 by pool-1-thread-1Fixed - Task 4 by pool-1-thread-2=== 2. Cached Thread Pool ===Cached - Task 1 by pool-2-thread-1Cached - Task 2 by pool-2-thread-2Cached - Task 3 by pool-2-thread-3Cached - Task 4 by pool-2-thread-4=== 3. Single Thread Executor ===Single - Task 1 by pool-3-thread-1Single - Task 2 by pool-3-thread-1Single - Task 3 by pool-3-thread-1=== 4. Scheduled Thread Pool ===Scheduling task to run after 1 second...Scheduling repeating task (every 2 seconds)...Repeating task tick at 1234567890123Delayed task executed!Repeating task tick at 1234567892123Repeating task tick at 1234567894123All executor demos complete!*/Future and Callable - Getting Results from Tasks
Sometimes you need your workers to return results! Runnable tasks can't return values, but Callable tasks can. The Future object is like a receipt - you can check if the task is done and get the result when it's ready. Perfect for computations!
// Using Callable and Future to get task resultsimport java.util.concurrent.*;import java.util.ArrayList;import java.util.List;public class CallableFutureExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); List<Future<Integer>> futures = new ArrayList<>(); System.out.println("Submitting calculation tasks..."); // Submit 5 calculation tasks that return results for (int i = 1; i <= 5; i++) { final int number = i; // Callable returns a value (unlike Runnable) Callable<Integer> task = () -> { System.out.println("Calculating factorial of " + number + " in " + Thread.currentThread().getName()); Thread.sleep(1000); // Simulate complex calculation // Calculate factorial int result = 1; for (int j = 1; j <= number; j++) { result *= j; } System.out.println("Factorial of " + number + " = " + result); return result; }; // Submit returns a Future - a "promise" of a future result Future<Integer> future = executor.submit(task); futures.add(future); } System.out.println("\nAll tasks submitted. Waiting for results...\n"); // Collect all results int sum = 0; for (int i = 0; i < futures.size(); i++) { try { // get() blocks until result is ready Integer result = futures.get(i).get(); System.out.println("Got result #" + (i + 1) + ": " + result); sum += result; } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } System.out.println("\nSum of all factorials: " + sum); executor.shutdown(); try { executor.awaitTermination(5, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("All calculations complete!"); }}/* Output:Submitting calculation tasks...All tasks submitted. Waiting for results...Calculating factorial of 1 in pool-1-thread-1Calculating factorial of 2 in pool-1-thread-2Calculating factorial of 3 in pool-1-thread-3Factorial of 1 = 1Calculating factorial of 4 in pool-1-thread-1Got result #1: 1Factorial of 2 = 2Calculating factorial of 5 in pool-1-thread-2Got result #2: 2Factorial of 3 = 6Got result #3: 6Factorial of 4 = 24Got result #4: 24Factorial of 5 = 120Got result #5: 120Sum of all factorials: 153All calculations complete!BENEFITS:- Can get return values from tasks- Future.get() waits for task completion- Can check if task is done without blocking- Can cancel tasks if needed*/CompletionService - Processing Results as They Arrive
CompletionService is smart! Instead of waiting for results in submission order, you can process them as they complete. It's like a restaurant where you serve dishes as they're cooked (fastest first), not in the order they were ordered!
// Using CompletionService to process results as they completeimport java.util.concurrent.*;public class CompletionServiceExample { public static void main(String[] args) { ExecutorService executor = Executors.newFixedThreadPool(3); CompletionService<String> completionService = new ExecutorCompletionService<>(executor); System.out.println("Submitting download tasks...\n"); // Simulate downloading files of different sizes String[] files = {"small.txt", "large.mp4", "medium.jpg", "tiny.pdf", "huge.zip"}; int[] downloadTimes = {1000, 5000, 3000, 500, 7000}; // Submit all download tasks for (int i = 0; i < files.length; i++) { final String fileName = files[i]; final int time = downloadTimes[i]; completionService.submit(() -> { System.out.println("Downloading " + fileName + "..."); Thread.sleep(time); System.out.println(fileName + " downloaded!"); return fileName; }); } System.out.println("All downloads started!\n"); System.out.println("Processing downloads as they complete...\n"); // Process results as they complete (not in submission order!) for (int i = 0; i < files.length; i++) { try { // take() returns the next completed task Future<String> result = completionService.take(); String fileName = result.get(); System.out.println("✓ Processed: " + fileName + " (completed #" + (i + 1) + ")"); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } } System.out.println("\nAll downloads processed in order of completion!"); executor.shutdown(); try { executor.awaitTermination(10, TimeUnit.SECONDS); } catch (InterruptedException e) { e.printStackTrace(); } }}/* Output:Submitting download tasks...Downloading small.txt...Downloading large.mp4...Downloading medium.jpg...All downloads started!Processing downloads as they complete...Downloading tiny.pdf...Downloading huge.zip...tiny.pdf downloaded!✓ Processed: tiny.pdf (completed #1)small.txt downloaded!✓ Processed: small.txt (completed #2)medium.jpg downloaded!✓ Processed: medium.jpg (completed #3)large.mp4 downloaded!✓ Processed: large.mp4 (completed #4)huge.zip downloaded!✓ Processed: huge.zip (completed #5)All downloads processed in order of completion!NOTICE: tiny.pdf finished first (500ms) even thoughit was submitted 4th! We processed results immediatelyas they became available, not in submission order.*/Real-World Example: Parallel Image Processing
Let's build a practical image processing system using executors! This simulates a real application that processes multiple images in parallel - like a photo editing app that applies filters to many photos at once. Shows the power of thread pools!
// Real-world parallel image processorimport java.util.concurrent.*;import java.util.List;import java.util.ArrayList;class ImageProcessor { private ExecutorService executor; private int processedCount = 0; public ImageProcessor(int threadPoolSize) { this.executor = Executors.newFixedThreadPool(threadPoolSize); System.out.println("Image Processor initialized with " + threadPoolSize + " worker threads\n"); } public Future<String> processImage(String imageName, String filter) { return executor.submit(() -> { String threadName = Thread.currentThread().getName(); System.out.println("[" + threadName + "] Processing: " + imageName + " with " + filter + " filter"); // Simulate image processing time int processingTime = 1000 + (int)(Math.random() * 2000); Thread.sleep(processingTime); String result = imageName + "_" + filter + ".jpg"; System.out.println("[" + threadName + "] Completed: " + result + " (" + processingTime + "ms)"); synchronized (this) { processedCount++; } return result; }); } public int getProcessedCount() { return processedCount; } public void shutdown() { executor.shutdown(); try { if (executor.awaitTermination(30, TimeUnit.SECONDS)) { System.out.println("\nAll image processing completed!"); } else { System.out.println("\nTimeout waiting for tasks to complete"); executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); } }}public class ImageProcessingExample { public static void main(String[] args) { long startTime = System.currentTimeMillis(); // Create processor with 4 worker threads ImageProcessor processor = new ImageProcessor(4); // List of images to process String[] images = { "vacation1.jpg", "vacation2.jpg", "vacation3.jpg", "portrait1.jpg", "portrait2.jpg", "landscape1.jpg", "sunset.jpg", "architecture.jpg", "nature.jpg", "city.jpg" }; // List of filters to apply String[] filters = {"Sepia", "Grayscale", "Blur", "Sharpen"}; List<Future<String>> futures = new ArrayList<>(); System.out.println("=== Starting Batch Image Processing ===\n"); // Submit processing tasks for each image for (String image : images) { String filter = filters[(int)(Math.random() * filters.length)]; Future<String> future = processor.processImage(image, filter); futures.add(future); } System.out.println("\nAll " + images.length + " tasks submitted to thread pool\n"); System.out.println("Workers are processing images in parallel...\n"); // Wait for all results List<String> results = new ArrayList<>(); for (Future<String> future : futures) { try { String result = future.get(); results.add(result); } catch (InterruptedException | ExecutionException e) { System.err.println("Error processing image: " + e.getMessage()); } } // Shutdown and wait processor.shutdown(); long endTime = System.currentTimeMillis(); double totalSeconds = (endTime - startTime) / 1000.0; // Print summary System.out.println("\n=== Processing Summary ==="); System.out.println("Total images processed: " + processor.getProcessedCount()); System.out.println("Total time: " + String.format("%.2f", totalSeconds) + " seconds"); System.out.println("\nProcessed images:"); for (int i = 0; i < results.size(); i++) { System.out.println(" " + (i + 1) + ". " + results.get(i)); } double avgTime = totalSeconds / images.length; System.out.println("\nAverage time per image: " + String.format("%.2f", avgTime) + " seconds"); System.out.println("(Would take ~" + String.format("%.1f", images.length * 1.5) + " seconds without parallel processing!)"); }}/* Output:Image Processor initialized with 4 worker threads=== Starting Batch Image Processing ===All 10 tasks submitted to thread poolWorkers are processing images in parallel...[pool-1-thread-1] Processing: vacation1.jpg with Blur filter[pool-1-thread-2] Processing: vacation2.jpg with Sepia filter[pool-1-thread-3] Processing: vacation3.jpg with Grayscale filter[pool-1-thread-4] Processing: portrait1.jpg with Sharpen filter[pool-1-thread-1] Completed: vacation1.jpg_Blur.jpg (1234ms)[pool-1-thread-1] Processing: portrait2.jpg with Sepia filter[pool-1-thread-3] Completed: vacation3.jpg_Grayscale.jpg (1567ms)[pool-1-thread-3] Processing: landscape1.jpg with Blur filter[pool-1-thread-2] Completed: vacation2.jpg_Sepia.jpg (1890ms)[pool-1-thread-2] Processing: sunset.jpg with Grayscale filter[pool-1-thread-4] Completed: portrait1.jpg_Sharpen.jpg (2100ms)[pool-1-thread-4] Processing: architecture.jpg with Sepia filter[pool-1-thread-1] Completed: portrait2.jpg_Sepia.jpg (1456ms)[pool-1-thread-1] Processing: nature.jpg with Blur filter[pool-1-thread-3] Completed: landscape1.jpg_Blur.jpg (1789ms)[pool-1-thread-3] Processing: city.jpg with Sharpen filter...All image processing completed!=== Processing Summary ===Total images processed: 10Total time: 6.45 secondsProcessed images: 1. vacation1.jpg_Blur.jpg 2. vacation2.jpg_Sepia.jpg 3. vacation3.jpg_Grayscale.jpg ... 10. city.jpg_Sharpen.jpgAverage time per image: 0.65 seconds(Would take ~15.0 seconds without parallel processing!)BENEFITS: 4 workers processed 10 images in ~6 secondsinstead of ~15 seconds sequentially!*/Key Concepts
Thread Pool Benefits
Reusing threads is much more efficient than creating new ones for each task. Thread pools reduce overhead, control resource usage, and provide better task management with queuing and scheduling capabilities.
ExecutorService Types
Fixed pool (constant threads), cached pool (flexible, grows/shrinks), single thread (sequential execution), and scheduled pool (delayed/periodic tasks). Choose based on your workload characteristics.
Callable vs Runnable
Runnable.run() returns void - good for tasks without results. Callable.call() returns a value and can throw checked exceptions - use when you need task results or error handling.
Future Interface
Represents a pending result. Can check if done (isDone), cancel execution (cancel), or block for result (get). Essential for coordinating asynchronous computations.
Best Practices
- ✓Always shutdown executors when done - use shutdown() for graceful termination or shutdownNow() for immediate
- ✓Size your thread pool appropriately: CPU-bound tasks ≈ CPU cores, I/O-bound tasks can have more threads
- ✓Use Callable and Future when you need return values instead of Runnable
- ✓Always use awaitTermination after shutdown to ensure all tasks complete before proceeding
- ✓Use CompletionService when you want to process results as they complete, not in submission order
- ✓Handle InterruptedException and ExecutionException properly when calling Future.get()
Common Mistakes to Avoid
- ✗Forgetting to shutdown the executor - threads keep running and prevent JVM shutdown
- ✗Creating too many threads in pool - wastes memory. Too few - wastes CPU. Tune based on workload!
- ✗Calling Future.get() in a loop immediately after submit - defeats the purpose of parallel execution
- ✗Not handling exceptions from Future.get() - tasks can fail silently
- ✗Using shutdownNow() without checking interrupted status in tasks
- ✗Submitting blocking tasks to a small thread pool - can cause thread starvation
Interview Tips
- 💡Know the difference between shutdown() (graceful) and shutdownNow() (forceful) and when to use each
- 💡Understand the four main executor types and their use cases (fixed, cached, single, scheduled)
- 💡Be able to explain why thread pools are better than creating new threads for each task
- 💡Know that Callable returns a value and Future represents a pending result
- 💡Understand how to size thread pools: CPU-bound (≈ CPU cores) vs I/O-bound (can be higher)
- 💡Be prepared to write code showing executor usage with proper shutdown and error handling