Appearance
Thread Pool
Although the Java language has built-in support for multithreading, starting a new thread is very convenient. However, creating threads consumes operating system resources (such as thread resources and stack space), and frequently creating and destroying a large number of threads can be time-consuming.
If we can reuse a set of threads:
┌─────┐ execute ┌──────────────────┐
│Task1│─────────▶│ThreadPool │
├─────┤ │┌───────┐┌───────┐│
│Task2│ ││Thread1││Thread2││
├─────┤ │└───────┘└───────┘│
│Task3│ │┌───────┐┌───────┐│
├─────┤ ││Thread3││Thread4││
│Task4│ │└───────┘└───────┘│
├─────┤ └──────────────────┘
│Task5│
├─────┤
│Task6│
└─────┘
...
Then we can have a group of threads execute many small tasks instead of creating a new thread for each task. This mechanism that can receive a large number of small tasks and distribute them for processing is called a thread pool.
Simply put, a thread pool internally maintains several threads that remain in a waiting state when there are no tasks. When new tasks arrive, an idle thread is assigned to execute the task. If all threads are busy, new tasks are either placed in a queue to wait or a new thread is created to handle them.
The Java standard library provides the ExecutorService
interface to represent a thread pool. Its typical usage is as follows:
java
// Create a fixed-size thread pool:
ExecutorService executor = Executors.newFixedThreadPool(3);
// Submit tasks:
executor.submit(task1);
executor.submit(task2);
executor.submit(task3);
executor.submit(task4);
executor.submit(task5);
Since ExecutorService
is just an interface, the Java standard library provides several common implementation classes:
- FixedThreadPool: A thread pool with a fixed number of threads.
- CachedThreadPool: A thread pool that dynamically adjusts the number of threads based on the number of tasks.
- SingleThreadExecutor: A thread pool that uses a single thread for execution.
These thread pool creation methods are encapsulated within the Executors
class. Let's take FixedThreadPool
as an example to examine the execution logic of a thread pool:
java
// thread-pool
import java.util.concurrent.*;
public class Main {
public static void main(String[] args) {
// Create a fixed-size thread pool:
ExecutorService es = Executors.newFixedThreadPool(4);
for (int i = 0; i < 6; i++) {
es.submit(new Task("" + i));
}
// Shutdown the thread pool:
es.shutdown();
}
}
class Task implements Runnable {
private final String name;
public Task(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println("start task " + name);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.println("end task " + name);
}
}
When observing the execution results, we submit 6 tasks at once. Since the thread pool only has a fixed size of 4 threads, the first 4 tasks will execute simultaneously. The remaining two tasks will wait until a thread becomes available before they are executed.
When the program ends, the thread pool must be shut down. Using the shutdown()
method to close the thread pool will wait for the currently executing tasks to complete before shutting down. The shutdownNow()
method will immediately stop all executing tasks, and awaitTermination()
will wait for a specified amount of time for the thread pool to shut down.
If we change the thread pool to a CachedThreadPool
, since this thread pool implementation dynamically adjusts the size based on the number of tasks, all 6 tasks can execute simultaneously.
If we want to limit the thread pool size to dynamically adjust between 4 and 10 threads, we can look at the source code of Executors.newCachedThreadPool()
:
java
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(
0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Therefore, to create a thread pool with a specified dynamic range, you can write:
java
int min = 4;
int max = 10;
ExecutorService es = new ThreadPoolExecutor(
min, max,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
ScheduledThreadPool
There is another type of task that needs to be executed periodically, such as refreshing stock prices every second. These tasks are fixed and need to be executed repeatedly. For such scenarios, you can use a ScheduledThreadPool. Tasks submitted to a ScheduledThreadPool
can be executed periodically.
Creating a ScheduledThreadPool
is also done through the Executors
class:
java
ScheduledExecutorService ses = Executors.newScheduledThreadPool(4);
We can submit one-time tasks that will execute once after a specified delay:
java
// Execute a one-time task after 1 second:
ses.schedule(new Task("one-time"), 1, TimeUnit.SECONDS);
If a task needs to execute at a fixed rate of every 3 seconds, you can write:
java
// Start executing a fixed-rate task after 2 seconds, then every 3 seconds:
ses.scheduleAtFixedRate(new Task("fixed-rate"), 2, 3, TimeUnit.SECONDS);
If a task needs to execute with a fixed delay of 3 seconds between the end of one execution and the start of the next, you can write:
java
// Start executing a fixed-delay task after 2 seconds, then with a 3-second interval:
ses.scheduleWithFixedDelay(new Task("fixed-delay"), 2, 3, TimeUnit.SECONDS);
Note the difference between FixedRate and FixedDelay:
- FixedRate means the task is triggered at fixed time intervals regardless of how long the task takes to execute:
│░░░░ │░░░░░░ │░░░ │░░░░░ │░░░
├───────┼───────┼───────┼───────┼────▶
│◀─────▶│◀─────▶│◀─────▶│◀─────▶│
- FixedDelay means that after the previous task completes, the scheduler waits for a fixed time interval before executing the next task:
│░░░│ │░░░░░│ │░░│ │░
└───┼───────┼─────┼───────┼──┼───────┼──▶
│◀─────▶│ │◀─────▶│ │◀─────▶│
Therefore, when using ScheduledThreadPool
, you should choose between one-time execution, fixed-rate execution, or fixed-delay execution based on your specific needs.
Additional Considerations
Attentive readers might ponder the following questions:
In FixedRate mode, if a task is triggered every second but one execution takes longer than one second, will subsequent tasks execute concurrently?
Yes, in
FixedRate
mode, if a task execution takes longer than the fixed interval, subsequent executions may overlap and execute concurrently.If a task throws an exception, will subsequent tasks continue to execute?
In most cases, if a task throws an unchecked exception, it will terminate the execution of that task, and subsequent tasks may not execute as expected. It's important to handle exceptions within tasks to ensure the thread pool continues to function correctly.
The Java standard library also provides the java.util.Timer
class, which can be used to execute tasks periodically. However, a Timer
corresponds to a single thread, meaning one Timer
can only execute one task periodically. Multiple periodic tasks would require multiple Timer
instances. In contrast, a ScheduledThreadPool
can schedule multiple periodic tasks using a single pool of threads. Therefore, ScheduledThreadPool
can effectively replace the older Timer
.
Exercise
Reuse threads using a thread pool.
Summary
The JDK provides ExecutorService
to implement thread pool functionality:
- Efficient Execution of Numerous Small Tasks: The thread pool internally maintains a set of threads that can efficiently execute a large number of small tasks.
- Static Methods for Creating Different Types of
ExecutorService
: TheExecutors
class offers static methods to create various types of thread pools. - Shutdown
ExecutorService
Properly: It is essential to callshutdown()
to close theExecutorService
. - ScheduledThreadPool for Periodic Task Scheduling:
ScheduledThreadPool
can schedule multiple periodic tasks efficiently.
Using thread pools can significantly improve the performance and resource management of multithreaded applications by reusing threads and managing task execution efficiently.