A thread pool is a collection of worker threads that are managed by the Java Virtual Machine (JVM). When a task is submitted to the thread pool, it is picked up by an available worker thread for execution. Once the task is completed, the thread becomes available again to execute another task. This way, threads are reused instead of being created and destroyed for each individual task.
Java provides the ExecutorService
interface and the Executors
class to create and manage thread pools. Here is an example of creating a fixed - size thread pool with 5 threads:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
// Create a fixed-size thread pool with 5 threads
ExecutorService executorService = Executors.newFixedThreadPool(5);
}
}
Once the thread pool is created, we can submit tasks to it. Tasks can be submitted in the form of Runnable
or Callable
objects. Here is an example of submitting a Runnable
task:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
// Create a Runnable task
Runnable task = () -> {
System.out.println("Task is running on thread: " + Thread.currentThread().getName());
};
// Submit the task to the thread pool
executorService.submit(task);
// Shutdown the thread pool
executorService.shutdown();
}
}
It is important to shut down the thread pool when it is no longer needed to release system resources. There are two methods to shut down a thread pool: shutdown()
and shutdownNow()
. The shutdown()
method initiates an orderly shutdown in which previously submitted tasks are executed, but no new tasks will be accepted. The shutdownNow()
method attempts to stop all actively executing tasks, halts the processing of waiting tasks, and returns a list of the tasks that were awaiting execution.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(5);
// Submit tasks...
// Shutdown the thread pool
executorService.shutdown();
}
}
A fixed - size thread pool has a predefined number of threads. Once all the threads are busy, new tasks are queued until a thread becomes available. This type of thread pool is suitable for applications where the number of concurrent tasks is relatively stable.
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 < 5; i++) {
final int taskNumber = i;
executorService.submit(() -> {
System.out.println("Task " + taskNumber + " is running on thread: " + Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
A cached thread pool creates new threads as needed, but will reuse previously created threads if they are available. If a thread has not been used for 60 seconds, it is terminated and removed from the pool. This type of thread pool is suitable for applications with a large number of short - lived asynchronous tasks.
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++) {
final int taskNumber = i;
executorService.submit(() -> {
System.out.println("Task " + taskNumber + " is running on thread: " + Thread.currentThread().getName());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executorService.shutdown();
}
}
A scheduled thread pool is used to execute tasks at a specified time or at regular intervals. It can be created using the Executors.newScheduledThreadPool()
method.
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(1);
// Schedule a task to run after 2 seconds
scheduledExecutorService.schedule(() -> {
System.out.println("Scheduled task is running on thread: " + Thread.currentThread().getName());
}, 2, TimeUnit.SECONDS);
// Schedule a task to run every 1 second after an initial delay of 1 second
scheduledExecutorService.scheduleAtFixedRate(() -> {
System.out.println("Repeated task is running on thread: " + Thread.currentThread().getName());
}, 1, 1, TimeUnit.SECONDS);
// Shutdown the scheduled thread pool after 5 seconds
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
scheduledExecutorService.shutdown();
}
}
The size of the thread pool depends on the nature of the tasks and the available system resources. For CPU - bound tasks, the optimal thread pool size is usually equal to the number of available CPU cores. For I/O - bound tasks, the thread pool size can be larger because the threads spend a significant amount of time waiting for I/O operations to complete.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class OptimalThreadPoolSize {
public static void main(String[] args) {
int cpuCores = Runtime.getRuntime().availableProcessors();
ExecutorService executorService = Executors.newFixedThreadPool(cpuCores);
}
}
Exceptions thrown in tasks submitted to a thread pool can be silently ignored if not handled properly. To handle exceptions, we can use a try - catch
block inside the Runnable
or Callable
task. Alternatively, we can use a custom ThreadFactory
to set an UncaughtExceptionHandler
for each thread in the pool.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExceptionHandlingThreadPool {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(() -> {
try {
throw new RuntimeException("Exception in task");
} catch (RuntimeException e) {
System.out.println("Caught exception: " + e.getMessage());
}
});
executorService.shutdown();
}
}
Monitoring the performance of a thread pool can help us identify bottlenecks and optimize the application. We can use the ThreadPoolExecutor
class directly instead of the Executors
factory methods to access more detailed information about the thread pool, such as the number of active threads, the size of the task queue, and the number of completed tasks.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
public class ThreadPoolMonitoring {
public static void main(String[] args) {
ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(5);
// Submit tasks...
System.out.println("Active threads: " + executorService.getActiveCount());
System.out.println("Task queue size: " + executorService.getQueue().size());
System.out.println("Completed tasks: " + executorService.getCompletedTaskCount());
executorService.shutdown();
}
}
Thread pools in Java are a powerful tool for efficient resource management in multithreaded applications. By understanding the fundamental concepts, usage methods, common practices, and best practices, developers can create high - performance and reliable applications. Properly configured thread pools can reduce resource consumption, improve performance, and simplify thread management.