“This is the 14th day of my participation in the First Challenge 2022.
1. The background
Many people in the process of using thread pools must have found that when a thread pool is defined in a thread, the main thread has ended but the pool is still running. This may not be so noticeable in Web development, but it’s easy to see in normal Java programs. Here is an example:
public class Test {
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(2);
executorService.execute(() -> {
for(;;) {try {
System.out.println("King Nima.");
TimeUnit.SECONDS.sleep(1);
} catch(InterruptedException e) { e.printStackTrace(); }}}); System.out.println("Complete"); }}Copy the code
When main is finished, the program ends.
But that’s not the case. The program is still running. Run the program directly to verify that:
By running the program in IDEA, it can be seen that “Wang Nima” is still printing after “finished” is printed, and the Stop button in the upper right corner of IDEA is not gray (indicating that the program is running). So what happens? Let’s explore this phenomenon by examining the source code for Java thread pools.
2. Source code analysis
JDK version information:
LTS Java(TM) SE Runtime Environment 18.9 (Build 11.0.12+8-LTS-237) Java HotSpot(TM) 64-bit Server VM 18.9 (Build 11.0.12+8-LTS-237, mixed mode)
Creating a thread pool is primarily about creating ThreadPoolExecutor instance objects. Look at the parameters that need to be set by analyzing its constructor:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
Copy the code
To summarize these parameters:
- Set the number of threads in the thread pool, and the lifetime of non-core threads
- The work queue of the thread pool
- The thread name of the thread pool
- Rejection policies
After the thread pool object is created, we submit the Runnable to the thread pool:
- Whether the number of working threads is smaller than the number of core threads
- Wrap the thread as a Worker and run Runable
The ThreadPoolExecutor#addWorker method has this code:
- Convert Runable to Worker
- Start the thread to which the Worker belongs
It can be seen from the above that Worker is the key. Take a look at Worker’s source code:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable{
// Omit some code
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
}
Copy the code
The AQS and Runable interfaces are implemented by definition, which have two important attributes:
- Thread: stores the thread bound to the current Worker
- FirstTask: store Runable for Worker instantiation (null if executed once)
With the Worker constructor:
Create a Thread by fetching the ThreadFactory of ThreadPoolExecutor#getThreadFactory, and bind Worker to Thread as a Runable instance object.
When the thread object bound to the Worker starts, the Worker’s run method is executed.
The work #run method is executed when the thread is started:
//Worker#run
public void run(a) {
runWorker(this);
}
Copy the code
The final call to ThreadPoolExecutor#runWorker is:
-
Check whether the task is empty. If it is the Worker’s first execution, the task will not be empty
-
If the task is empty, it is fetched from the workQueue
The workQueue is set by the constructor when ThreadPoolExecutor is instantiated and is a blocking queue.
-
Execute task, since task is a Runable object, call task.run directly.
-
Set task to NULL when execution is complete
With that said, the crux of today’s question is: Why is the Java thread pool still running when the main thread ends?
There is a while loop in the ThreadPoolExecutor#runWorker method, and the conditions for this while loop are:
while(task ! =null|| (task = getTask()) ! =null)
Copy the code
That’s an interesting condition. What’s interesting about it?
- The first time the task is executed, it is certain that the task is not empty and does not perform subsequent judgments
- On the second execution, the task must be empty (because task=null is set in finally), and the task will be fetched from the workQueue blocking queue. If not, the currently executing worker will block waiting for the task. Once it gets it, it goes through the while loop.
The above while loop explicitly states that it is always True under normal circumstances, so the Worker thread will keep executing.
3. Summary
The thread is bound to the Worker, and then cleverly uses the blocking queue to construct a always true While loop condition, so that the Worker’s thread is always executing the Task in the blocking queue in a loop. This is why thread pools continue to run after the main thread ends. In layman’s terms, the core thread that performs the task is in an infinite loop, but the loop is artificially constructed.