Home/Java/Executors in Java

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:

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// Without Executors - inefficient thread management
public 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-0
Task 1 completed
Task 2 started by Thread-1
Task 2 completed
Task 3 started by Thread-2
Task 3 completed
...
Task 10 completed
Total time: ~10000ms
Created 10 threads (expensive!)
PROBLEMS:
1. Created 10 threads - lots of memory overhead
2. Tasks run sequentially because we wait for each
3. Manual thread management is error-prone
4. 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// Using ExecutorService with fixed thread pool
import 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-1
Task 2 started by pool-1-thread-2
Task 3 started by pool-1-thread-3
Task 1 completed by pool-1-thread-1
Task 4 started by pool-1-thread-1
Task 2 completed by pool-1-thread-2
Task 5 started by pool-1-thread-2
Task 3 completed by pool-1-thread-3
Task 6 started by pool-1-thread-3
...
All tasks completed!
Total time: ~4000ms
Only created 3 threads (reused for all tasks!)
BENEFITS:
1. Only 3 threads created (saves memory!)
2. Threads are reused for multiple tasks
3. Automatic task queue management
4. 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// Exploring different types of Executors
import 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-1
Fixed - Task 2 by pool-1-thread-2
Fixed - Task 3 by pool-1-thread-1
Fixed - Task 4 by pool-1-thread-2
=== 2. Cached Thread Pool ===
Cached - Task 1 by pool-2-thread-1
Cached - Task 2 by pool-2-thread-2
Cached - Task 3 by pool-2-thread-3
Cached - Task 4 by pool-2-thread-4
=== 3. Single Thread Executor ===
Single - Task 1 by pool-3-thread-1
Single - Task 2 by pool-3-thread-1
Single - 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 1234567890123
Delayed task executed!
Repeating task tick at 1234567892123
Repeating task tick at 1234567894123
All 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
// Using Callable and Future to get task results
import 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-1
Calculating factorial of 2 in pool-1-thread-2
Calculating factorial of 3 in pool-1-thread-3
Factorial of 1 = 1
Calculating factorial of 4 in pool-1-thread-1
Got result #1: 1
Factorial of 2 = 2
Calculating factorial of 5 in pool-1-thread-2
Got result #2: 2
Factorial of 3 = 6
Got result #3: 6
Factorial of 4 = 24
Got result #4: 24
Factorial of 5 = 120
Got result #5: 120
Sum of all factorials: 153
All 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// Using CompletionService to process results as they complete
import 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 though
it was submitted 4th! We processed results immediately
as 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!

java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
// Real-world parallel image processor
import 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 pool
Workers 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: 10
Total time: 6.45 seconds
Processed images:
1. vacation1.jpg_Blur.jpg
2. vacation2.jpg_Sepia.jpg
3. vacation3.jpg_Grayscale.jpg
...
10. city.jpg_Sharpen.jpg
Average time per image: 0.65 seconds
(Would take ~15.0 seconds without parallel processing!)
BENEFITS: 4 workers processed 10 images in ~6 seconds
instead 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