Executor
framework and thread pools to simplify the process of managing and executing multiple threads. This blog will guide you through the fundamental concepts, usage methods, common practices, and best practices of Java Executors and thread pools.The Executor
interface is the base of the Java Executor framework. It provides a simple way to decouple task submission from task execution. The Executor
interface has only one method: execute(Runnable command)
, which is used to execute a given task.
import java.util.concurrent.Executor;
class DirectExecutor implements Executor {
@Override
public void execute(Runnable command) {
command.run();
}
}
ExecutorService
is a sub - interface of Executor
that provides more features for managing tasks and the executor itself. It allows you to submit tasks that return results (Callable
tasks), shut down the executor gracefully, and check the status of tasks.
A thread pool is a collection of pre - created threads that are ready to execute tasks. Instead of creating a new thread for each task, the tasks are submitted to the thread pool, and the threads in the pool pick up the tasks and execute them. Thread pools help reduce the overhead of creating and destroying threads, improving the performance of the application.
The Executors
class provides several factory methods to create different types of thread pools.
A fixed - size thread pool has a specified number of threads. If a task is submitted when all threads are busy, the task is placed in a queue.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
});
}
executor.shutdown();
}
}
A cached thread pool creates new threads as needed but reuses previously created threads if they are available. If a thread remains idle for too long, it is terminated.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class CachedThreadPoolExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running on thread " + Thread.currentThread().getName());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
});
}
executor.shutdown();
}
}
You can submit tasks to an ExecutorService
using the submit()
method. It accepts both Runnable
and Callable
tasks.
import java.util.concurrent.*;
public class TaskSubmissionExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
// Submitting a Runnable task
executor.submit(() -> System.out.println("Runnable task is running."));
// Submitting a Callable task
Future<Integer> future = executor.submit(() -> {
Thread.sleep(1000);
return 42;
});
Integer result = future.get();
System.out.println("Callable task result: " + result);
executor.shutdown();
}
}
When your application is about to exit, it is important to shut down the ExecutorService
gracefully. You can use the shutdown()
method to initiate a graceful shutdown, which allows previously submitted tasks to execute but does not accept new tasks. If you want to wait for all tasks to complete, you can use awaitTermination()
.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class GracefulShutdownExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
for (int i = 0; i < 3; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
});
}
executor.shutdown();
try {
if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
}
When using Callable
tasks, exceptions can be handled through the Future
object. If an exception occurs during the execution of a Callable
task, it will be wrapped in an ExecutionException
when you call Future.get()
.
import java.util.concurrent.*;
public class ExceptionHandlingExample {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(() -> {
throw new RuntimeException("Something went wrong!");
});
try {
Integer result = future.get();
} catch (InterruptedException | ExecutionException e) {
System.out.println("Exception: " + e.getCause().getMessage());
}
executor.shutdown();
}
}
Executors
Factory Methods in ProductionThe factory methods provided by the Executors
class are convenient but may have some drawbacks. For example, newCachedThreadPool()
can create an unbounded number of threads, which may lead to resource exhaustion. In production, it is recommended to use the ThreadPoolExecutor
constructor directly to create a thread pool with more fine - grained control.
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class CustomThreadPoolExample {
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // core pool size
5, // maximum pool size
60, // keep - alive time
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10) // work queue
);
for (int i = 0; i < 15; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " is running.");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task " + taskId + " is completed.");
});
}
executor.shutdown();
}
}
The size of the thread pool should be set according to the nature of the tasks. For CPU - bound tasks, the number of threads should be close to the number of available CPU cores. For I/O - bound tasks, a larger number of threads can be used to keep the CPU busy while waiting for I/O operations to complete.
int cpuCores = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor cpuBoundExecutor = new ThreadPoolExecutor(
cpuCores,
cpuCores,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>()
);
Java Executors and thread pools are powerful tools for managing and executing multiple threads in a Java application. By understanding the fundamental concepts, usage methods, common practices, and best practices, you can write more efficient and robust multithreaded applications. Remember to use thread pools carefully, especially in production environments, to avoid resource exhaustion and other issues.