線程池關閉不合理,導致應用無法正常stop的情況

在上一篇博客中,我使用了線程池進行管理線程,達到線程複用的效果。

詳情參考:線程池+CountDownLatch優化代碼,提高程序執行效率

 

程序啓動、運行皆無異常,線程池確實對程序中創建的線程進行管理,但是,在我關閉tomcat時,無法正常關閉,程序出現報錯。報錯信息如下:

05-Apr-2020 19:09:45.003 璀﹀憡 [localhost-startStop-2] org.apache.catalina.loader.WebappClassLoaderBase.clearReferencesThreads 
The web application [ROOT] appears to have started a thread named [pool-5-thread-2] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
 sun.misc.Unsafe.park(Native Method)
 java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
 java.util.concurrent.LinkedBlockingDeque.takeFirst(LinkedBlockingDeque.java:492)
 java.util.concurrent.LinkedBlockingDeque.take(LinkedBlockingDeque.java:680)
 java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1067)
 java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1127)
 java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
 java.lang.Thread.run(Thread.java:748

 注意看這行報錯信息:

The web application [ROOT] appears to have started a thread named [pool-5-thread-2] but has failed to stop it. This is very likely to create a memory leak. 

異常信息顯示: 應用程序啓動了一個名爲pool-5-thread-2的線程,但是關閉失敗,這很可能導致內存泄漏。

很明顯,tomcat沒能關掉ThreadPoolExecutor的核心線程,因此需要在關閉tomcat前手動關閉。

如何手動關閉線程池請參考:停止Tomcat webapp 線程池報錯的嘗試解決

那如果我不想手動關閉線程池怎麼辦?

使用Spring中中的 ThreadPoolTaskExecutor。將線程池的關閉交給Spring去管理。

在applicationContext.xml中加入

 <!-- spring thread pool executor -->
    <bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
        <!-- 線程池維護線程的最少數量 -->
        <property name="corePoolSize" value="10"/>
        <!-- 允許的空閒時間 -->
        <property name="keepAliveSeconds" value="5"/>
        <!-- 線程池維護線程的最大數量 -->
        <property name="maxPoolSize" value="20"/>
        <!-- 緩存隊列 -->
        <property name="queueCapacity" value="4"/>
        <!-- 對拒絕task的處理策略 -->
        <property name="rejectedExecutionHandler">
            <bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy"/>
        </property>
    </bean>

程序中:

    @Autowired
    private ThreadPoolTaskExecutor threadPool;

     threadPool.execute(() -> {
            model.addAttribute("types", typeService.findHottestType(6));
            countDownLatch.countDown();
        });
     threadPool.execute(() -> {
            model.addAttribute("tags", tagService.findHottestTag(6));
            countDownLatch.countDown();
        });
     threadPool.execute(() -> {
            model.addAttribute("blogs", blogService.findHotestBlogs());
            countDownLatch.countDown();
        });
     threadPool.execute(() -> {
            model.addAttribute("newBlogs", blogService.findNewestBlogs(8));
            countDownLatch.countDown();
        });

再次啓動程序,測試,關閉tomcat,報錯信息不再提示。

 

如何優雅安全的關閉線程池?

線程池關閉不合理,導致應用⽆法正常stop的情況,這是我們很容易忽略的。

下面就以ThreadPoolExecutor爲例,來介紹下如何優雅的關閉線程池。

線程中斷

 

在介紹線程池關閉之前,先介紹下Thread的interrupt。

在程序中,我們是不能隨便中斷⼀個線程的,因爲這是極其不安全的操作,我們⽆法知道這個線程正運⾏在什麼狀態,它可能持有某把鎖,強⾏中斷可能導致鎖不能釋放的問題;或者線程可能在操作數據庫,強⾏中斷導致數據不一致,從而混亂的問題。正因此,Java⾥將Thread的stop⽅法設置爲過時,以禁⽌⼤家使⽤。

⼀個線程什麼時候可以退出呢?當然只有線程自⼰才能知道。

所以我們這⾥要說的Thread的interrrupt⽅法,本質不是⽤來中斷一個線程。而是將線程設置⼀箇中斷狀態。當我們調⽤線程的interrupt方法,它有兩個作⽤用:

1、如果此線程處於阻塞狀態(比如調⽤了wait方法,io等待),則會立刻退出阻塞,並拋出InterruptedException異常,線程就可以通過捕獲InterruptedException來做⼀定的處理,然後讓線程退出。

2、如果此線程正處於運行之中,則線程不受任何影響,繼續運行,僅僅是線程的中斷標記被設置爲true。所以線程要在適當的位置通過調用isInterrupted方法來查看自⼰是否被中斷,並做退出操作。

注:如果線程的interrupt方法先被調用,然後線程調用阻塞方法進入阻塞狀態,InterruptedException異常依舊會拋出。如果線程捕獲InterruptedException異常後,繼續調用阻塞方法, 將不再觸發InterruptedException異常。

線程池的關閉

 

線程池提供了兩個關閉方法,shutdownNow和shuwdown⽅法。

shutdownNow⽅法的解釋是:線程池拒接收新提交的任務,同時⽴刻關閉線程池,線程池里的任務不再執行

shutdown⽅法的解釋是:線程池拒接收新提交的任務,同時等待線程池⾥的任務執行完畢後關閉線程池。

以上的說法雖然沒錯,但是還有很多的細節問題,比如調用shutdown⽅法後,正在執⾏的任務的線程會做出什麼反應?正在等待任務的線程又會做出什麼反應?線程在什麼情況下才會徹底退出。如果不瞭解這些細節,在關閉線程池時就難免遇到,像線程池關閉不了,關閉線程池出現報錯等情況。

再說這些關閉線程池細節問題之前,需要強調一點的是,調用完shutdownNow和shuwdown⽅法後,並不代表線程池已經完成關閉操作,它只是異步的通知線程池進行關閉處理。如果要同步等待線程池徹底關閉後才繼續往下執行,需要調⽤awaitTermination⽅法進⾏同步等待。

 

我們來看看shutdownNow和shuwdown⽅法的源碼:

shutdown:

/**
     * 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();
        // 將線程池運行狀態設置爲SHUTDOWN
        advanceRunState(SHUTDOWN);
        // 中斷所有空閒線程
        interruptIdleWorkers();
        // 此方法在ThreadPoolExecutor中是空實現,具體實現在其子類ScheduledThreadPoolExecutor
        // 中,用於取消延時任務。
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    // 嘗試將線程池置爲TERMINATED狀態
    tryTerminate();
}

 

 

 

 

 

 

shutdownNow:

    /**
     * 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();
        // 將線程池運行狀態置爲STOP
        advanceRunState(STOP);
        // 中斷所有線程,包括正在運行的線程
        interruptWorkers();
        // 將未執行的任務移入列表中
        tasks = drainQueue();
    } finally {
        mainLock.unlock();
    }
    // 嘗試將線程池置爲TERMINATED狀態
    tryTerminate();
    return tasks;
}

具體源碼解釋請參考:關閉線程池的正確姿勢

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章