Java provides ExecutorService to manage asynchronous tasks and Future to track their results. Together, they let you submit work, wait for results, and manage task execution without handling each thread directly.
Table of Contents
- 1. Understanding ExecutorService
- 2. How to instantiate an ExecutorService
- 3. Example: Using ExecutorService
- 4. Introducing Future
- 5. Example: Using Future
- 6. Best Practices
- 7. Conclusion
1. Understanding ExecutorService
The ExecutorService interface is part of the java.util.concurrent package, and represents a pool of threads that can be used to execute tasks concurrently. ExecutorService handles thread creation and management through a higher-level API for asynchronous task execution.
1.1 Key Features of ExecutorService
- Thread Pool Management:
ExecutorServicemanages a pool of threads, reusing them for executing multiple tasks. This approach reduces the overhead associated with thread creation and destruction. - Task Submission: Tasks can be submitted to the
ExecutorServiceusing various methods likeexecute(Runnable command)for fire-and-forget tasks orsubmit(Callable<T> task)for tasks that return a result. - Graceful Shutdown:
ExecutorServiceprovides methods likeshutdown()andshutdownNow()to stop the execution of tasks gracefully or forcefully.
2. How to instantiate an ExecutorService
Java provides several ways to instantiate an ExecutorService, each suited to different use cases. The Executors utility class is commonly used to create different types of thread pools.
2.1 Single Thread Executor
A single-thread executor creates an ExecutorService that uses a single worker thread to execute tasks sequentially.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingleThreadExecutorExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executorService.execute(new Task(i));
}
executorService.shutdown();
}
}
class Task implements Runnable {
private final int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Executing task " + taskId
+ " by " + Thread.currentThread().getName());
}
}2.2 Fixed Thread Pool
A fixed thread pool creates an ExecutorService with a specified number of threads.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.execute(new Task(i));
}
executorService.shutdown();
}
}2.3 Cached Thread Pool
A cached thread pool creates an ExecutorService that can dynamically create new threads as needed, but will reuse previously constructed threads when available.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
executorService.execute(new Task(i));
}
executorService.shutdown();
}
}2.4 Scheduled Thread Pool
A scheduled thread pool creates an ExecutorService that can schedule commands to run after a given delay, or to execute periodically.
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class ScheduledThreadPoolExample {
public static void main(String[] args) {
ScheduledExecutorService scheduledExecutorService =
Executors.newScheduledThreadPool(2);
for (int i = 0; i < 5; i++) {
scheduledExecutorService.schedule(new Task(i), 2, TimeUnit.SECONDS);
}
scheduledExecutorService.shutdown();
}
}2.5 Work Stealing Pool
A work stealing pool creates an ExecutorService that maintains enough threads to support a given parallelism level and uses multiple queues to reduce contention.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class WorkStealingPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newWorkStealingPool();
for (int i = 0; i < 10; i++) {
executorService.submit(new Task(i));
}
executorService.shutdown();
}
}2.6 Custom Thread Pool
In some cases, you may need more control over the thread pool configuration, such as setting custom thread factories or handlers for rejected tasks.
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
ThreadPoolExecutor executorService =
(ThreadPoolExecutor) Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
executorService.execute(new Task(i));
}
executorService.shutdown();
try {
executorService.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}3. Example: Using ExecutorService
This example runs multiple I/O-bound operations concurrently.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExecutorServiceExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10; i++) {
executorService.execute(new Task(i));
}
executorService.shutdown();
}
}
class Task implements Runnable {
private final int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
@Override
public void run() {
System.out.println("Executing task " + taskId
+ " by " + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}4. Introducing Future
The Future interface represents the result of an asynchronous computation. It provides methods to check if the computation is complete, wait for its completion, and retrieve the result.
4.1 Key Features of Future
- Result Retrieval:
Futureallows you to retrieve the result of a computation once it's done using theget()method. This method blocks until the result is available. - Cancellation: You can cancel the execution of a task using the
cancel(boolean mayInterruptIfRunning)method. - Status Check:
Futureprovides methods likeisDone()to check if the task is completed andisCancelled()to check if the task was cancelled.
5. Example: Using Future
The next version uses Future to handle tasks that return results.
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
public class FutureExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
Future<Integer>[] futures = new Future[10];
for (int i = 0; i < 10; i++) {
futures[i] = executorService.submit(new TaskWithResult(i));
}
for (Future<Integer> future : futures) {
try {
System.out.println("Result: " + future.get());
} catch (Exception e) {
e.printStackTrace();
}
}
executorService.shutdown();
try {
executorService.awaitTermination(1, TimeUnit.MINUTES);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class TaskWithResult implements Callable<Integer> {
private final int taskId;
public TaskWithResult(int taskId) {
this.taskId = taskId;
}
@Override
public Integer call() {
System.out.println("Executing task " + taskId
+ " by " + Thread.currentThread().getName());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return taskId * 2;
}
}6. Best Practices
- Proper Shutdown: Always ensure that the
ExecutorServiceis properly shut down usingshutdown()orshutdownNow()to free up resources. - Handling Exceptions: Be mindful of exceptions in tasks. Use appropriate exception handling in the
callorrunmethods to avoid unexpected crashes. - Timeouts: Use timeouts with the
getmethod ofFutureto prevent indefinite blocking. - Resource Management: Be cautious with the number of threads in the pool. Too many threads can lead to resource exhaustion, while too few can cause performance bottlenecks.
- Avoiding Deadlocks: Ensure tasks do not hold locks or resources for long periods, as this can lead to deadlocks.
7. Conclusion
ExecutorService handles thread management and task submission. Future lets you retrieve results, check task status, and cancel work.
Shut down executors when they are no longer needed, handle task exceptions, and use timeouts to avoid waiting indefinitely. Choose the pool size with resource use and performance in mind.
Test Your Understanding
Question 1 of 5 · Score: 0/0 correct
Your first answer counts toward your score.
