[Java] How to close the thread pool gracefully

background

A few days ago, I was chatting with my colleagues about a requirement, saying that there is a data query function, because it involves multiple third-party interface calls, I want to use the thread pool to do it in parallel.

It is a very normal solution, but after going online, it is found that every time the service is released, the data query function will hang up, and later it is found that the thread pool has not been closed properly, here is a summary.

Keywords: thread pool, shutdown, shutdownNow, interrupt

1. Thread interrupt interrupt

Make up for the basic knowledge first: thread interruption.
The meaning of thread interruption is not to force the running thread to "click" to interrupt, but to set the interrupt flag position of the thread to true, so that when the thread is blocked (wait, join, sleep), InterruptedException will be thrown , the program does some aftermath by catching InterruptedException, and then lets the thread exit.

Let’s take a look at an example. The following code starts a thread and prints a hundred lines of text. During the printing process, the interrupt flag of the thread will be set to true

public static void test02() throws InterruptedException {
    
    

    Thread t = new Thread(() -> {
    
    
    for (int i = 0; i < 100; i++) {
    
    
        System.out.println("process i=" + i + ",interrupted:" + Thread.currentThread().isInterrupted());
    }
    });
    t.start();
    Thread.sleep(1);
    t.interrupt();
}

Looking at the output of the console, I found that the interrupt flag has been successfully set to true when 57 is printed, but the thread is still printing, indicating that the interrupt flag is only set, rather than interrupting the thread directly.

...
process i=55,interrupted:false
process i=56,interrupted:false
process i=57,interrupted:true
process i=58,interrupted:true
process i=59,interrupted:true
...

Look at this example again, it is also printing one hundred lines of text, the interrupt flag will be judged during the printing process, and it will exit by itself if it is interrupted.

public static void test02() throws InterruptedException {
    
    

    Thread t = new Thread(() -> {
    
    
    for (int i = 0; i < 100; i++) {
    
    
        if (Thread.interrupted()) {
    
    
            System.out.println("线程已中断,退出执行");
            break;
        }
        System.out.println("process i=" + i + ",interrupted:" + Thread.currentThread().isInterrupted());
    }
    });
    t.start();
    Thread.sleep(1);
    t.interrupt();
}

The console output is as follows:

process i=49,interrupted:false
process i=50,interrupted:false
process i=51,interrupted:false

The thread has been interrupted, exiting execution

2. The shutdown method of the thread pool

After understanding thread interruption, let's take a look at how to close the thread pool.

There are two methods shutdown() and shutdownNow() to close the thread pool. What is the difference? Let's take a look at the shutdown() method first

 /**
     * Initiates an orderly shutdown in which previously submitted
     * tasks are executed, but no new tasks will be accepted.
     * Invocation has no additional effect if already shut down.
     *
     * <p>This method does not wait for previously submitted tasks to
     * complete execution.  Use {@link #awaitTermination awaitTermination}
     * to do that.
     *
     * @throws SecurityException {@inheritDoc}
     */
    public void shutdown() {
    
    
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            checkShutdownAccess();
            advanceRunState(SHUTDOWN); // 1. 把线程池的状态设置为 SHUTDOWN
            interruptIdleWorkers(); // 2. 把空闲的工作线程置为中断
            onShutdown(); // 3. 一个空实现,暂不用关注
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
    }

Look at the source code and read the comments first, and translate it:

Initiating an orderly shutdown executes previously submitted tasks but does not accept any new tasks.
If already closed, the call has no additional effect.
This method does not wait for the tasks executed by the activity to terminate. This can be done using awaitTermination() if desired.

2.1. The first step: advanceRunState(SHUTDOWN) set the thread pool to SHUTDOWN

The state flow of the thread pool is as follows. Calling the shutdown() method will set the state of the thread pool to SHUTDOWN, and subsequent submission of tasks to the thread pool will be rejected (judged in the execute() method).

insert image description here

2.2, the second step: interruptIdleWorkers() sets the idle worker thread to interrupt

The interruptIdleWorkers() method traverses all worker threads, and if tryLock() succeeds, the thread is set to interrupt.
Here, if tryLock() succeeds, it means that the corresponding woker is an idle thread that is not executing tasks. If it fails, it means that the corresponding worker is executing tasks. In other words, the interruption here has no effect on the tasks being executed.

 private void interruptIdleWorkers(boolean onlyOne) {
    
    
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            for (Worker w : workers) {
    
    
                Thread t = w.thread;
                if (!t.isInterrupted() && w.tryLock()) {
    
    
                    try {
    
    
                        t.interrupt();
                    } catch (SecurityException ignore) {
    
    
                    } finally {
    
    
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
    
    
            mainLock.unlock();
        }
    }

2.3, the third step: onShutdown() is an empty implementation, don't pay attention to it for now

This is nothing, just a way to leave blank.
insert image description here

2.4. Summary

The shutdown() method does two things:

  • Set the thread pool state to SHUTDOWN state
  • interrupt idle thread

Let's look at an example to deepen our impression.

public static void test01() throws InterruptedException {
    
    

        // corePoolSize 是 2,maximumPoolSize 是 2
        ThreadPoolExecutor es = new ThreadPoolExecutor(2, 2,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>());
        es.prestartAllCoreThreads(); // 启动所有 worker
        es.execute(new Task()); // Task是一个访问某网站的 HTTP 请求,跑的慢,后面会贴出来完整代码,这里把他当做一个跑的慢的异步任务就行
        es.shutdown();
        es.execute(new Task()); // 在线程池 shutdown() 后 继续添加任务,这里预期是抛出异常
    }

In this example, we mainly observe two phenomena.

  • One is that the thread pool will have two wokers (the call of the prestartAllCoreThreads() method makes two workers already started), one of which is executing and one is idle. So when calling the shutdown() method and entering interruptIdleWorkers(), only the idle thread will call t.interrupt().
    insert image description here

  • The second is that after calling the shutdown() method, when calling execute(), an exception will be thrown, because the state of the thread pool has been set to SHUTDOWN, and no new tasks will be added.

3. The shutdownNow method of closing the thread pool

 /**
     * Attempts to stop all actively executing tasks, halts the
     * processing of waiting tasks, and returns a list of the tasks
     * that were awaiting execution. These tasks are drained (removed)
     * from the task queue upon return from this method.
     *
     * <p>This method does not wait for actively executing tasks to
     * terminate.  Use {@link #awaitTermination awaitTermination} to
     * do that.
     *
     * <p>There are no guarantees beyond best-effort attempts to stop
     * processing actively executing tasks.  This implementation
     * cancels tasks via {@link Thread#interrupt}, so any task that
     * fails to respond to interrupts may never terminate.
     *
     * @throws SecurityException {@inheritDoc}
     */
    public List<Runnable> shutdownNow() {
    
    
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            checkShutdownAccess();
            advanceRunState(STOP); // 1:把线程池设置为STOP
            interruptWorkers(); // 2.中断工作线程
            tasks = drainQueue(); // 3.把线程池中的任务都 drain 出来
        } finally {
    
    
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

Annotation means:

Attempts to stop all executing tasks, suspends processing of waiting tasks, and returns a list of tasks waiting to execute. These tasks are emptied (removed) from the task queue upon return from this method.
This method does not wait for the tasks executed by the activity to terminate. This can be done using awaitTermination() if desired.
There are no guarantees other than a best-effort attempt to stop processing actively executing tasks.
This implementation cancels tasks via Thread.Interrupt(), so any tasks that fail to respond to interrupts may never terminate.

3.1. The first step: advanceRunState() sets the thread pool to STOP

Unlike the shutdown() method, the shutdownNow() method will set the state of the thread pool to STOP.

3.2. The second step: interruptWorkers() interrupts the worker thread

interruptWorkers() is as follows, you can see that, unlike the shutdown() method, all worker threads call the interrupt() method

  /**
     * Interrupts all threads, even if active. Ignores SecurityExceptions
     * (in which case some threads may remain uninterrupted).
     */
    private void interruptWorkers() {
    
    
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
    
    
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
    
    
            mainLock.unlock();
        }
    }

3.3, the third step: drainQueue() drains all the tasks in the thread pool

The drainQueue() method is as follows, take out all the tasks waiting in the blocking queue and return. When closing the thread pool, you can print all the returned tasks based on this feature and make a record.

  /**
     * Drains the task queue into a new list, normally using
     * drainTo. But if the queue is a DelayQueue or any other kind of
     * queue for which poll or drainTo may fail to remove some
     * elements, it deletes them one by one.
     */
    private List<Runnable> drainQueue() {
    
    
        BlockingQueue<Runnable> q = workQueue;
        ArrayList<Runnable> taskList = new ArrayList<Runnable>();
        q.drainTo(taskList);
        if (!q.isEmpty()) {
    
    
            for (Runnable r : q.toArray(new Runnable[0])) {
    
    
                if (q.remove(r))
                    taskList.add(r);
            }
        }
        return taskList;
    }

3.4 Summary

The shutdownNow() method does three things:

  • Set the thread pool state to STOP state
  • interrupt worker thread
  • Drain all the tasks in the thread pool and return

Let's take a look at an example, the code is the same as before, but the thread is closed using shutdownNow()

public static void test01() throws InterruptedException {
    
    

        // corePoolSize 是 1,maximumPoolSize 是 1,无限容量
        ThreadPoolExecutor es = new ThreadPoolExecutor(1, 1,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>());
        es.prestartAllCoreThreads(); // 启动所有 worker
        es.execute(new Task()); // Task是一个访问某网站的 HTTP 请求,跑的慢,后面会贴出来完整代码,这里把他当做一个跑的慢的异步任务就行
        es.execute(new Task());
        List<Runnable> result = es.shutdownNow();
        System.out.println(result);
        es.execute(new Task()); // 在线程池 shutdownNow() 后 继续添加任务,这里预期是抛出异常
    }

In this example, we mainly observe three phenomena.
One is that there are two wokers in the thread pool, so when calling the shutdownNow() method and entering interruptWorkers(), all wokers will call t.interrupt().
insert image description here

The second is that the shutdownNow() method will return tasks that have not yet been executed and print them out.
The third is that after calling the shutdownNow() method, when calling execute(), an exception will be thrown, because the state of the thread pool has been set to STOP, and new tasks will no longer be added
insert image description here

4. Actual combat, cooperate with JVM hook

In actual work, we generally use the shutdown() method because it is "gentle" and waits for us to finish executing all the tasks in the thread pool. The shutdown() method is also used as an example here.

Let's go back to the case we talked about at the beginning. The machine is re-released, but the thread pool still has not finished executing tasks. When the machine is shut down, all these tasks will be killed. What should we do? Is there any mechanism that can block it and wait for the task to be executed before closing it?

Yes, with JVM hooks!

The sample code is as follows, a thread pool submits three tasks for execution, and the execution takes half a minute. Then add a JVM hook, which can be simply understood as a listener. After registration, the JVM will call this method when it is closed, and the JVM will be officially closed after the call.

public static void test01() throws InterruptedException {
    
    

        ThreadPoolExecutor es = new ThreadPoolExecutor(1, 1,
                60L, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>());

        es.execute(new Task());
        es.execute(new Task());
        es.execute(new Task());


        Thread shutdownHook = new Thread(() -> {
    
    
            es.shutdown();
            try {
    
    
                es.awaitTermination(3, TimeUnit.MINUTES);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
                System.out.println("等待超时,直接关闭");
            }
        });
        Runtime.getRuntime().addShutdownHook(shutdownHook);
    }

Execute on the machine, you will find that I use ctrl + c (note not ctrl + z) to close the process, you will find that the process is not closed directly, the thread pool is still running, and the process will not be formal until the task of the thread pool is executed quit.
insert image description here

How about it, isn't it amazing.
The source code of the Task involved in this article is as follows. This task is to initiate 10 requests to the stackoverflow website to simulate a slow task. Of course, this is not the point and can be ignored. Students who are interested in trying the code in this article can refer to it.

 public static class Task implements Runnable {
    
    

        @Override
        public void run() {
    
    
            System.out.println("task start");
            for (int i = 0; i < 10; i++) {
    
    
                httpGet();
                System.out.println("task execute " + i);
            }
            System.out.println("task finish");
        }

        private void httpGet() {
    
    

            String url = "https://stackoverflow.com/";
            String result = "";
            BufferedReader in = null;
            try {
    
    
                String urlName = url;
                URL realUrl = new URL(urlName);
                // 打开和URL之间的连接
                URLConnection conn = realUrl.openConnection();
                // 设置通用的请求属性
                conn.setRequestProperty("accept", "*/*");
                conn.setRequestProperty("connection", "Keep-Alive");
                conn.setRequestProperty("user-agent",
                        "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)");
                // 建立实际的连接
                conn.connect();
                // 获取所有响应头字段
                Map<String, List<String>> map = conn.getHeaderFields();
//                 遍历所有的响应头字段
//                for (String key : map.keySet()) {
    
    
//                    System.out.println(key + "--->" + map.get(key));
//                }
                // 定义BufferedReader输入流来读取URL的响应
                in = new BufferedReader(
                        new InputStreamReader(conn.getInputStream()));
                String line;
                while ((line = in.readLine()) != null) {
    
    
                    result += "/n" + line;
                }
            } catch (Exception e) {
    
    
                e.printStackTrace();
            }
            // 使用finally块来关闭输入流
            finally {
    
    
                try {
    
    
                    if (in != null) {
    
    
                        in.close();
                    }
                } catch (Exception ex) {
    
    
                    ex.printStackTrace();
                }
            }
//            System.out.print(result);
        }
    }

V. Summary

If you want to shut down the thread pool gracefully, you must first understand the meaning of thread interruption.
Secondly, there are two ways to close the thread pool: shutdown() and shutdownNow(). The biggest difference between the two is that shutdown() only interrupts the idle woker, does not affect the running woker, and will continue to execute The task is finished. shutdownwNow() is to set all wokers to interrupt, extract and return all tasks to be executed, and shutdown() is more commonly used in daily work.
Finally, simply using shutdown() is not reliable, and you have to use awaitTermination() and JVM hooks to close the thread pool gracefully.

Guess you like

Origin blog.csdn.net/u011397981/article/details/131325310