Concurrent programming is essential for building high-performance applications that efficiently utilize multi-core processors. Java provides robust support for concurrency through the java.util.concurrent
package, and one of its key components is the ExecutorService
. This tutorial will guide you through the fundamentals of using ExecutorService
for concurrent programming in Java. We’ll cover its various features, how to create and manage tasks, and how to handle task completion and error management. By the end of this tutorial, you will have a comprehensive understanding of how to leverage ExecutorService
for your concurrent programming needs.
1. Introduction to ExecutorService
The ExecutorService
is part of the java.util.concurrent
package and provides a higher-level replacement for working directly with threads. It offers a framework for managing a pool of threads, scheduling tasks to be executed by these threads, and handling task completion and errors.
Key Features of ExecutorService
- Thread Pool Management: Manages a pool of worker threads, optimizing the execution of multiple tasks.
- Task Submission: Supports different ways to submit tasks, including
Runnable
,Callable
, andFuture
. - Task Scheduling: Allows scheduling tasks to run after a delay or periodically.
- Task Completion Handling: Provides mechanisms to retrieve the results of tasks and handle their completion.
- Error Management: Includes features for handling exceptions and errors that occur during task execution.
2. Creating an ExecutorService
You can create an ExecutorService
using the Executors
factory methods provided by the java.util.concurrent
package. Here are some common ways to create an ExecutorService
:
Fixed Thread Pool
A fixed thread pool creates a pool with a fixed number of threads.
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);
Code language: Java (java)
Cached Thread Pool
A cached thread pool creates new threads as needed and reuses previously constructed threads when available.
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
Code language: Java (java)
Single Thread Executor
A single-thread executor ensures that tasks are executed sequentially.
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
Code language: Java (java)
Scheduled Thread Pool
A scheduled thread pool is used for scheduling tasks to run after a delay or periodically.
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4);
Code language: Java (java)
3. Submitting Tasks to ExecutorService
Tasks can be submitted to an ExecutorService
in various forms, such as Runnable
, Callable
, and Future
.
Submitting Runnable Tasks
A Runnable
task does not return a result and cannot throw a checked exception.
Runnable task = () -> {
System.out.println("Executing task");
};
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.submit(task);
Code language: Java (java)
Submitting Callable Tasks
A Callable
task returns a result and can throw a checked exception.
Callable<Integer> task = () -> {
return 123;
};
Future<Integer> future = executorService.submit(task);
Code language: Java (java)
Handling Futures
You can retrieve the result of a Callable
task using the Future
object.
try {
Integer result = future.get(); // This will block until the result is available
System.out.println("Result: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
Code language: Java (java)
Submitting Multiple Tasks
You can submit multiple tasks to the ExecutorService
and wait for their completion using invokeAll
.
List<Callable<Integer>> tasks = Arrays.asList(
() -> 1,
() -> 2,
() -> 3
);
List<Future<Integer>> futures = executorService.invokeAll(tasks);
for (Future<Integer> future : futures) {
System.out.println("Result: " + future.get());
}
Code language: Java (java)
4. Managing Task Completion
Managing task completion involves handling results and dealing with task timeouts.
Handling Timeouts
You can specify a timeout when retrieving the result from a Future
.
try {
Integer result = future.get(1, TimeUnit.SECONDS); // Wait for 1 second
System.out.println("Result: " + result);
} catch (TimeoutException e) {
System.out.println("Task timed out");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
Code language: Java (java)
Waiting for Task Completion
You can wait for all tasks to complete using the awaitTermination
method.
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
Code language: Java (java)
5. Handling Errors in ExecutorService
Error handling is crucial in concurrent programming to ensure that exceptions in one task do not affect others.
Handling Exceptions in Runnable Tasks
Since Runnable
tasks cannot throw checked exceptions, you need to handle exceptions within the task.
Runnable task = () -> {
try {
// Task logic
} catch (Exception e) {
System.out.println("Exception in task: " + e.getMessage());
}
};
executorService.submit(task);
Code language: Java (java)
Handling Exceptions in Callable Tasks
Callable
tasks can throw checked exceptions, which are propagated to the Future
.
Callable<Integer> task = () -> {
if (true) {
throw new Exception("Exception in task");
}
return 123;
};
Future<Integer> future = executorService.submit(task);
try {
Integer result = future.get();
} catch (ExecutionException e) {
System.out.println("Exception in task: " + e.getCause().getMessage());
} catch (InterruptedException e) {
e.printStackTrace();
}
Code language: Java (java)
6. Advanced Usage of ExecutorService
Scheduled Tasks
You can schedule tasks to run after a delay or periodically using ScheduledExecutorService
.
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
// Schedule a task to run after a 1-second delay
scheduledExecutorService.schedule(() -> {
System.out.println("Task executed after delay");
}, 1, TimeUnit.SECONDS);
// Schedule a task to run periodically every 2 seconds
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Periodic task executed");
}, 0, 2, TimeUnit.SECONDS);
Code language: Java (java)
Custom Thread Factory
You can create a custom thread factory to control the creation of threads.
ThreadFactory customThreadFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "CustomThread-" + threadNumber.getAndIncrement());
thread.setDaemon(true);
return thread;
}
};
ExecutorService customExecutorService = Executors.newFixedThreadPool(2, customThreadFactory);
customExecutorService.submit(() -> {
System.out.println(Thread.currentThread().getName() + " executing task");
});
Code language: Java (java)
ExecutorCompletionService
ExecutorCompletionService
combines an Executor
with a BlockingQueue
to retrieve the results of completed tasks.
ExecutorCompletionService<Integer> completionService = new ExecutorCompletionService<>(executorService);
List<Callable<Integer>> tasks = Arrays.asList(
() -> 1,
() -> 2,
() -> 3
);
for (Callable<Integer> task : tasks) {
completionService.submit(task);
}
for (int i = 0; i < tasks.size(); i++) {
try {
Future<Integer> future = completionService.take(); // Retrieves and removes the next completed task
System.out.println("Result: " + future.get());
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
Code language: Java (java)
7. Best Practices for Using ExecutorService
Proper Shutdown
Always shut down the ExecutorService
to release resources.
executorService.shutdown();
try {
if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
Code language: Java (java)
Handling Uncaught Exceptions
Use an UncaughtExceptionHandler
to handle uncaught exceptions in threads.
ThreadFactory threadFactory = new ThreadFactory() {
private final AtomicInteger threadNumber = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "Thread-" + threadNumber.getAndIncrement());
thread.setUncaughtExceptionHandler((t, e) -> {
System.out.println("Uncaught exception in thread " + t.getName() + ": " + e.getMessage());
});
return thread;
}
};
ExecutorService executorServiceWithHandler = Executors.newFixedThreadPool(2, threadFactory);
executorServiceWithHandler.submit(() -> {
throw new RuntimeException("Exception in task");
});
Code language: Java (java)
Tuning Thread Pool Size
Properly tune the thread pool size based on the nature of tasks (CPU-bound or I/O-bound).
- CPU-bound tasks: Number of threads should be equal to the number of available processors.
- I/O-bound tasks: Number of threads should be higher than the number of available processors, typically 2 times or more.
int numberOfProcessors = Runtime.getRuntime().availableProcessors();
ExecutorService cpuBoundExecutor = Executors.newFixedThreadPool(numberOfProcessors);
ExecutorService ioBoundExecutor = Executors.newFixedThreadPool(numberOfProcessors * 2);
Using Future with Timeouts
Use timeouts with Future.get()
to prevent indefinite blocking.
try {
Integer result = future.get(1, TimeUnit.SECONDS);
System.out.println("Result: " + result);
} catch (TimeoutException e) {
System.out.println("Task timed out");
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
Code language: Java (java)
Graceful Degradation
Implement graceful degradation for tasks that can tolerate failure.
Callable<Integer> resilientTask = () -> {
try {
// Simulate task logic
return 123;
} catch (Exception e) {
// Log and return a default value
System.out.println("Exception in task: " + e.getMessage());
return -1;
}
};
Future<Integer> future = executorService.submit(resilientTask);
Code language: Java (java)
8. Conclusion
Using ExecutorService
in Java simplifies the management of concurrent tasks by abstracting the complexities of thread management. It provides a flexible and robust framework for submitting, managing, and monitoring tasks. By following the best practices outlined in this tutorial, you can effectively leverage ExecutorService
to build high-performance, scalable applications.