Java Executor Framework
An executor accepts tasks so application code does not need to manage a new thread for every job. ExecutorService adds task results and lifecycle operations. Choosing its queue, concurrency limit, and shutdown behavior is part of using it correctly.
Why Do We Need the Executor Framework?
When an application has many tasks to execute, creating a new thread for every task can become difficult to manage.
For example:
for (int i = 1; i <= 100; i++) {
Thread thread = new Thread(() -> {
// perform task
});
thread.start();
}This approach creates a large number of threads.
Creating too many threads can:
- Consume more memory
- Increase thread-management overhead
- Reduce application performance
- Make thread lifecycle management difficult
- Make it harder to control how many tasks execute simultaneously
The Executor Framework solves this problem by separating task submission from thread management.
We submit tasks, and the executor decides which available thread should execute them.
Executor Framework Architecture
The basic idea is:
Tasks
|
v
+----------------+
| Executor |
+----------------+
|
v
+--------------+
| Thread Pool |
+--------------+
/ | \
v v v
Thread Thread ThreadThe application submits tasks to the executor.
The executor manages a pool of threads and assigns tasks to available threads.
Important Interfaces and Classes
The Executor Framework mainly uses:
| Type | Purpose |
|---|---|
| Executor | Executes submitted tasks |
| ExecutorService | Provides lifecycle management and task submission features |
| ScheduledExecutorService | Executes tasks after a delay or periodically |
| Executors | Factory class for creating executor services |
| ThreadPoolExecutor | Configurable thread-pool implementation |
| ScheduledThreadPoolExecutor | Thread pool implementation for scheduled tasks |
| Future | Represents the result of an asynchronous computation |
| Callable | Represents a task that can return a result |
1. Executor Interface
Executor is the basic interface of the Executor Framework.
It provides one main method:
void execute(Runnable command);Example
import java.util.concurrent.Executor;
public class HelloWorld {
public static void main(String[] args) {
Executor executor = command -> {
new Thread(command).start();
};
executor.execute(() -> {
System.out.println("Task is running");
});
}
}Here, we submit a Runnable task using execute().
The Executor interface focuses only on executing tasks. It does not provide methods for shutting down the executor or obtaining task results.
2. ExecutorService
ExecutorService extends the Executor interface and provides additional functionality for managing tasks and the executor lifecycle.
Some commonly used methods are:
- execute()
- submit()
- shutdown()
- shutdownNow()
- isShutdown()
- isTerminated()
- awaitTermination()
- invokeAll()
- invokeAny()
Example
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HelloWorld {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(() -> {
System.out.println("Task 1");
});
executor.execute(() -> {
System.out.println("Task 2");
});
executor.shutdown();
}
}Here, Executors.newFixedThreadPool(2) creates an executor with a pool containing up to two worker threads.
ExecutorService as a Thread Pool
A thread pool is a collection of reusable worker threads.
For example:
ExecutorService
|
Thread Pool
/ | \
/ | \
Thread 1 Thread 2 Thread 3Suppose there are five tasks and only two worker threads:
Tasks:
T1 T2 T3 T4 T5
Thread Pool:
Thread 1
Thread 2Initially:
Thread 1 -> T1
Thread 2 -> T2When a thread finishes:
Thread 1 -> T3
Thread 2 -> T4Then:
Thread 1 -> T5
Thread 2 -> availableThe threads can be reused instead of creating a new thread for every task.
Creating a Fixed Thread Pool
A fixed-size thread pool can be created using:
ExecutorService executor =
Executors.newFixedThreadPool(3);This creates an executor that uses up to three worker threads.
Example
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class HelloWorld {
public static void main(String[] args) {
ExecutorService executor =
Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
int taskNumber = i;
executor.execute(() -> {
System.out.println(
"Executing Task " + taskNumber +
" by " + Thread.currentThread().getName()
);
});
}
executor.shutdown();
}
}The output order is not guaranteed because multiple tasks may execute concurrently.
execute() Method
The execute() method accepts a Runnable.
executor.execute(task);Example
executor.execute(() -> {
System.out.println("Task executed");
});It is useful when we only need to execute a task and do not need a result.
submit() Method
submit() is more flexible than execute().
It can accept:
- Runnable
- Callable
and returns a Future.
Example with Runnable:
ExecutorService executor =
Executors.newFixedThreadPool(2);
Future<?> future = executor.submit(() -> {
System.out.println("Task executed");
});
executor.shutdown();The returned Future can be used to track the submitted task.
Callable
Callable is similar to Runnable, but it can:
- Return a result
- Throw checked exceptions
Example
Callable<Integer> task = () -> {
return 10 + 20;
};We can submit it to an executor:
ExecutorService executor =
Executors.newFixedThreadPool(2);
Future<Integer> future = executor.submit(task);We can obtain the result using:
Integer result = future.get();Complete example:
import java.util.concurrent.*;
public class HelloWorld {
public static void main(String[] args)
throws Exception {
ExecutorService executor =
Executors.newFixedThreadPool(2);
Callable<Integer> task = () -> {
return 10 + 20;
};
Future<Integer> future =
executor.submit(task);
Integer result = future.get();
System.out.println("Result: " + result);
executor.shutdown();
}
}Output
Result: 30Future
Future represents the result of an asynchronous computation.
When we submit a Callable, we immediately receive a Future.
Future<Integer> future =
executor.submit(task);The task may still be running when submit() returns.
We can later retrieve its result:
Integer result = future.get();Some important methods of Future are:
| Method | Purpose |
|---|---|
| get() | Waits for and returns the result |
| get(timeout, unit) | Waits for a limited time |
| isDone() | Checks whether the task has completed |
| isCancelled() | Checks whether the task was cancelled |
| cancel() | Attempts to cancel the task |
Future.get() Can Block
The following statement:
future.get();waits if the task has not finished yet.
Example
Future<Integer> future = executor.submit(() -> {
Thread.sleep(3000);
return 100;
});
System.out.println(future.get());The calling thread waits until the task completes and produces the result.
A timeout can be used when we do not want to wait indefinitely:
Integer result =
future.get(2, TimeUnit.SECONDS);If the result is not available within the specified time, a TimeoutException can occur.
shutdown()
After submitting all required tasks, the executor should normally be shut down.
executor.shutdown();shutdown():
- Stops accepting new tasks
- Allows already submitted tasks to complete
- Begins an orderly shutdown
Example
executor.shutdown();It does not immediately interrupt currently running tasks.
shutdownNow()
shutdownNow() attempts to stop the executor more aggressively.
List<Runnable> tasks =
executor.shutdownNow();It:
- Attempts to interrupt running tasks
- Prevents queued tasks from starting
- Returns tasks that were awaiting execution
However, interruption is cooperative. A running task does not necessarily stop immediately just because shutdownNow() was called.
isShutdown()
Checks whether shutdown has been initiated.
executor.isShutdown();Example
executor.shutdown();
System.out.println(executor.isShutdown());Output
trueisTerminated()
Checks whether all tasks have completed after shutdown.
executor.isTerminated();A useful distinction is:
shutdown()
|
v
No new tasks accepted
|
v
Existing tasks complete
|
v
TerminatedawaitTermination()
awaitTermination() allows a thread to wait for the executor to terminate.
Example
executor.shutdown();
try {
if (executor.awaitTermination(10, TimeUnit.SECONDS)) {
System.out.println("Executor terminated");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Common Executor Types Created Using Executors
The Executors utility class provides factory methods for creating different executor configurations.
Common examples include:
Executors.newFixedThreadPool(5);
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
Executors.newScheduledThreadPool(3);Fixed Thread Pool
ExecutorService executor =
Executors.newFixedThreadPool(4);It uses a fixed number of worker threads.
Suitable when we want to limit the number of tasks executing concurrently.
Example
ExecutorService executor =
Executors.newFixedThreadPool(4);
for (int i = 1; i <= 10; i++) {
int task = i;
executor.submit(() -> {
System.out.println("Task " + task);
});
}
executor.shutdown();At most four worker threads are used by this executor for these tasks.
Single Thread Executor
ExecutorService executor =
Executors.newSingleThreadExecutor();It uses one worker thread.
Tasks are executed sequentially by that worker.
Example
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"));
executor.shutdown();The tasks are processed one at a time.
However, if task execution itself has asynchronous behavior or uses other threads, the overall application behavior can be more complex.
Cached Thread Pool
ExecutorService executor =
Executors.newCachedThreadPool();A cached thread pool can create new threads when needed and reuse previously created idle threads.
It is designed for workloads with many short-lived asynchronous tasks.
Example
ExecutorService executor =
Executors.newCachedThreadPool();
for (int i = 1; i <= 10; i++) {
int task = i;
executor.execute(() -> {
System.out.println("Task " + task);
});
}
executor.shutdown();Because its thread creation is not bounded by a fixed pool size, it should be used carefully for workloads where task submission can grow significantly.
Scheduled Executor Service
ScheduledExecutorService is used when tasks need to execute:
- After a delay
- At a fixed rate
- With a fixed delay between executions
Example
ScheduledExecutorService executor =
Executors.newScheduledThreadPool(2);Execute a Task After a Delay
executor.schedule(
() -> System.out.println("Task executed"),
5,
TimeUnit.SECONDS
);The task is scheduled to execute after approximately five seconds.
Execute a Task Periodically
executor.scheduleAtFixedRate(
() -> System.out.println("Running task"),
2,
5,
TimeUnit.SECONDS
);Here:
Initial delay = 2 seconds
Period = 5 secondsThe task is scheduled repeatedly according to the specified period.
Fixed Rate vs Fixed Delay
There are two commonly used scheduling methods:
- scheduleAtFixedRate()
- scheduleWithFixedDelay()
scheduleAtFixedRate()
Attempts to maintain a regular schedule based on the configured period.
Start → wait period → Start → wait period → StartscheduleWithFixedDelay()
Waits for the previous execution to finish and then waits for the specified delay before starting the next execution.
Start → Finish → wait delay → Start → FinishThis difference is important when task execution can take a significant amount of time.
invokeAll()
invokeAll() submits multiple Callable tasks and returns a list of Future objects.
Example
List<Callable<Integer>> tasks = List.of(
() -> 10,
() -> 20,
() -> 30
);
List<Future<Integer>> results =
executor.invokeAll(tasks);We can retrieve the results:
for (Future<Integer> future : results) {
System.out.println(future.get());
}Output
10
20
30invokeAll() waits for all submitted tasks to complete before returning, unless the timed form is used.
invokeAny()
invokeAny() submits multiple Callable tasks and returns the result of one successfully completed task.
Example
List<Callable<String>> tasks = List.of(
() -> "Task 1",
() -> "Task 2",
() -> "Task 3"
);
String result = executor.invokeAny(tasks);
System.out.println(result);The exact task that supplies the result can vary depending on execution timing.
ThreadPoolExecutor
ThreadPoolExecutor is a configurable implementation of ExecutorService.
It allows us to control important thread-pool properties such as:
- Core thread count
- Maximum thread count
- Keep-alive time
- Work queue
- Thread factory
- Rejected execution policy
Example
ThreadPoolExecutor executor =
new ThreadPoolExecutor(
2,
5,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);Here:
Core threads = 2
Maximum threads = 5
Keep alive = 60 seconds
Work queue = LinkedBlockingQueueThis provides much more control than the simple factory methods.
How ThreadPoolExecutor Processes Tasks
A simplified model is:
Task submitted
|
v
Are core threads available?
|
+---- Yes ----> Execute task
|
No
|
v
Is work queue available?
|
+---- Yes ----> Put task in queue
|
No
|
v
Can another thread be created?
|
+---- Yes ----> Create worker
|
No
|
v
Reject taskThe exact behavior depends on the executor configuration and queue capacity.
BlockingQueue in a Thread Pool
A ThreadPoolExecutor uses a BlockingQueue<Runnable> to hold tasks waiting for execution.
For example:
new LinkedBlockingQueue<>()Tasks can therefore be in different stages:
Submitted
|
v
Running
|
v
Completedor:
Submitted
|
v
Waiting in Queue
|
v
Running
|
v
CompletedRejectedExecutionHandler
A task can be rejected when an executor cannot accept it.
ThreadPoolExecutor supports different rejection policies.
Common policies include:
- AbortPolicy
- CallerRunsPolicy
- DiscardPolicy
- DiscardOldestPolicy
For example:
new ThreadPoolExecutor.CallerRunsPolicy()The rejection policy determines what happens when a task cannot be accepted.
ExecutorService vs Thread
Using Thread directly:
Thread thread = new Thread(() -> {
System.out.println("Task");
});
thread.start();Using ExecutorService:
ExecutorService executor =
Executors.newFixedThreadPool(2);
executor.execute(() -> {
System.out.println("Task");
});
executor.shutdown();The second approach separates the task from the mechanism used to execute it.
ExecutorService vs Runnable
Runnable represents the task:
Runnable task = () -> {
System.out.println("Task");
};ExecutorService manages execution:
executor.execute(task);So:
Runnable
↓
Represents the work
ExecutorService
↓
Manages executionExecutorService vs Callable
| Feature | Runnable | Callable |
|---|---|---|
| Method | run() | call() |
| Return value | No | Yes |
| Checked exception | Cannot declare through run() | Can throw |
| Common submission method | execute() / submit() | submit() |
| Result | Future<?> with submit() | Future<T> |
Example
Runnable runnable = () -> {
System.out.println("Running");
};
Callable<Integer> callable = () -> {
return 100;
};ExecutorService vs Executor
| Feature | Executor | ExecutorService |
|---|---|---|
| Task execution | Yes | Yes |
| execute() | Yes | Yes |
| submit() | No | Yes |
| Shutdown support | No | Yes |
| Future support | No | Yes |
| Batch operations | No | Yes |
Executor is the simpler abstraction, while ExecutorService provides additional task and lifecycle management.
Handling Exceptions in Executor Tasks
When using submit(), exceptions thrown by the task are captured and can be observed through Future.get().
Example
ExecutorService executor =
Executors.newFixedThreadPool(2);
Future<?> future = executor.submit(() -> {
throw new RuntimeException("Something went wrong");
});
try {
future.get();
} catch (ExecutionException e) {
System.out.println(
"Task failed: " + e.getCause()
);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
executor.shutdown();The exception can therefore be handled by the code retrieving the result.
Important: Always Shut Down the Executor
A common mistake is to create an executor and never shut it down.
For example:
ExecutorService executor =
Executors.newFixedThreadPool(5);After the required work is submitted, normally shut it down:
executor.shutdown();Otherwise, its worker threads may continue to keep the application alive.
Executor Framework with Try-Finally
A common pattern is to ensure shutdown happens even when an exception occurs:
ExecutorService executor =
Executors.newFixedThreadPool(3);
try {
executor.submit(() -> {
System.out.println("Task");
});
} finally {
executor.shutdown();
}For more complex applications, the executor lifecycle should generally be managed by the component that owns it.
Benefits of the Executor Framework
1. Thread Reuse
Worker threads can execute multiple tasks.
2. Better Thread Management
The framework manages worker threads instead of requiring manual creation for every task.
3. Controlled Concurrency
A thread pool can limit how many tasks execute concurrently.
4. Task and Thread Separation
The application focuses on tasks rather than directly managing individual threads.
5. Asynchronous Results
Future allows tasks to return results asynchronously.
6. Task Scheduling
ScheduledExecutorService supports delayed and periodic execution.
7. Better Scalability
A properly configured executor can handle many tasks without creating an unlimited number of threads.
Common Mistakes
1. Creating an Executor and Not Shutting It Down
ExecutorService executor =
Executors.newFixedThreadPool(5);Remember to manage its lifecycle.
2. Creating Too Many Threads
A larger pool is not automatically faster.
Too many concurrent tasks can cause:
- CPU contention
- Memory usage
- Context-switching overhead
- Resource contention
The appropriate pool configuration depends on the workload and application resources.
3. Calling Future.get() Too Early
For example:
Future<Integer> future =
executor.submit(task);
Integer result = future.get();If the task takes a long time, the calling thread waits.
Using a timeout can sometimes be more appropriate:
future.get(5, TimeUnit.SECONDS);4. Ignoring InterruptedException
Avoid silently ignoring interruption.
A common pattern is:
catch (InterruptedException e) {
Thread.currentThread().interrupt();
}Restoring the interrupted status allows higher-level code to respond appropriately.
5. Assuming Tasks Execute in Submission Order
Submitting tasks in this order:
Task 1
Task 2
Task 3does not necessarily mean they will finish in that order when multiple worker threads are available.
Executor Framework and Virtual Threads
Modern Java also provides virtual threads, which can be created through the concurrency APIs.
For example:
ExecutorService executor =
Executors.newVirtualThreadPerTaskExecutor();
try {
executor.submit(() -> {
System.out.println("Task");
});
} finally {
executor.close();
}Virtual threads are lightweight threads designed to support large numbers of concurrent tasks, particularly tasks that spend significant time waiting on blocking I/O.
They are different from traditional platform-thread pools, so choosing between a fixed platform-thread pool and virtual threads depends on the workload and application design.
Executor Framework vs Manual Thread Creation
Manual Thread Creation
Thread thread = new Thread(() -> {
System.out.println("Task");
});
thread.start();The application directly creates and starts the thread.
Executor Framework
ExecutorService executor =
Executors.newFixedThreadPool(3);
executor.execute(() -> {
System.out.println("Task");
});
executor.shutdown();The application submits the task, while the executor manages the worker threads.
Real-World Example
Suppose an application needs to process multiple files.
Without an executor:
File 1 → Thread 1
File 2 → Thread 2
File 3 → Thread 3
File 4 → Thread 4
...With a fixed thread pool:
Executor
|
Thread Pool
/ | \
/ | \
Thread 1 Thread 2 Thread 3
\ | /
\ | /
File TasksIf there are 100 files but only three worker threads, the executor can process tasks in batches rather than creating 100 application-managed threads.
Important Methods of ExecutorService
| Method | Description |
|---|---|
| execute() | Executes a Runnable |
| submit() | Submits a task and returns a Future |
| invokeAll() | Executes multiple Callable tasks and returns their futures |
| invokeAny() | Executes multiple Callable tasks and returns one successful result |
| shutdown() | Starts orderly shutdown |
| shutdownNow() | Attempts to stop running tasks and returns queued tasks |
| isShutdown() | Checks whether shutdown has started |
| isTerminated() | Checks whether all tasks have completed after shutdown |
| awaitTermination() | Waits for termination |
Important Points to Remember
- The Executor Framework is available in java.util.concurrent.
- Executor is the basic task-execution abstraction.
- ExecutorService provides task submission and lifecycle management.
- Runnable represents a task without a return value.
- Callable represents a task that can return a result.
- Future represents the result of an asynchronous computation.
- Thread pools reuse worker threads.
- shutdown() should normally be called when the executor is no longer needed.
- shutdownNow() attempts interruption but does not guarantee immediate termination.
- FixedThreadPool limits the number of worker threads.
- SingleThreadExecutor uses one worker thread.
- ScheduledExecutorService supports delayed and periodic execution.
- ThreadPoolExecutor provides detailed control over thread-pool behavior.
- Multiple tasks do not necessarily finish in submission order.
- The appropriate executor configuration depends on the workload.
- Modern Java also provides executors designed to work with virtual threads.
Key Takeaway
The Executor Framework provides a structured way to execute and manage concurrent tasks in Java.
Instead of manually creating a new Thread for every task, we can submit tasks to an executor:
Task
↓ExecutorService ↓ Thread Pool ↓ Worker Thread ↓ Task Execution
This separates what the application needs to do from how threads are created and managed, making concurrent applications easier to control and maintain.
See you in next page....🙂