如何優雅的關閉Java線程池


如何優雅的關閉Java線程池

面試中經常會問到,創建一個線程池需要哪些參數啊,線程池的工作原理啊,卻很少會問到線程池如何安全關閉的。

也正是因為大家不是很關注這塊,即便是工作三四年的人,也會有因為線程池關閉不合理,導致應用無法正常stop的情況,還有出現一些報錯的問題。

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

01 線程中斷

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

在程序中,我們是不能隨便中斷一個線程的,因為這是極其不安全的操作,我們無法知道這個線程正運行在什么狀態,它可能持有某把鎖,強行中斷可能導致鎖不能釋放的問題;或者線程可能在操作數據庫,強行中斷導致數據不一致混亂的問題。正因此,JAVA里將Thread的stop方法設置為過時,以禁止大家使用。

一個線程什么時候可以退出呢?當然只有線程自己才能知道。

所以我們這里要說的Thread的interrrupt方法,本質不是用來中斷一個線程。是將線程設置一個中斷狀態。

當我們調用線程的interrupt方法,它有兩個作用:

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

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

注:

如果線程的interrupt方法先被調用,然后線程調用阻塞方法進入阻塞狀態,InterruptedException異常依舊會拋出。

如果線程捕獲InterruptedException異常后,繼續調用阻塞方法,將不再觸發InterruptedException異常。

02 線程池的關閉

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

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

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

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

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

有了以上介紹,下面就結合線程池源碼,分別說說這兩個線程池關閉方法的一些實現細節。

shutdownNow

我們看一下shutdownNow方法的源碼:

在shutdownNow方法里,重要的三句代碼我用紅色數字標出來了。

第一句就是原子性的修改線程池的狀態為STOP狀態(比較簡單我就不貼代碼了)

第三句是將隊列里還沒有執行的任務放到列表里,返回給調用方。

第二句是遍歷線程池里的所有工作線程,然后調用線程的interrupt方法。如下圖:

 

以上就是shutdownNow方法的執行邏輯:將線程池狀態修改為STOP,然后調用線程池里的所有線程的interrupt方法。

調用shutdownNow后,線程池里的線程會做如何反應呢?那就要看,線程池里線程正在執行的代碼邏輯了。其在線程池的runWorker方法里(對線程池的執行原理不了解的,請看之前的文章),其代碼如下:

正常情況下,線程池里的線程,就是在這個while循環里不停地執行。其中代碼task.run()就是在執行我們提交給線程池的任務,如當我們調用shutdownNow時,task.run()里面正處於IO阻塞,則會導致報錯,如果task.run()里正在正常執行,則不受影響,繼續執行完這個任務。

從上圖看的出來,如果getTask()方法返回null,也會導致線程的退出。我們再來看看getTask方法的實現:

如果我們調用shutdownNow方法時,線程處於從隊列里讀取任務而阻塞中(圖中下邊的紅框),則會導致拋出InterruptedException異常,但因為異常被捕獲,線程將會繼續在這個for循環里執行。

還記得shutdownNow方法里將線程修改為STOP狀態吧,當執行到上邊紅框里的代碼時,由於STOP狀態值是大於SHUTDOWN狀態,STOP也大於等於STOP,不管任務隊列是否為空,都會進入if語句從而返回null,線程退出。

總結:

當我們調用線程池的shutdownNow時,

如果線程正在getTask方法中執行,則會通過for循環進入到if語句,於是getTask返回null,從而線程退出。不管線程池里是否有未完成的任務。

如果線程因為執行提交到線程池里的任務而處於阻塞狀態,則會導致報錯(如果任務里沒有捕獲InterruptedException異常),否則線程會執行完當前任務,然后通過getTask方法返回為null來退出。

shutdown

我們再來看看shutdown方法的源碼:

跟shutdownNow類似,只不過它是將線程池的狀態修改為SHUTDOWN狀態,然后調用interruptIdleWorkers方法,來中斷空閑的線程。這是interruptIdleWorkers方法的實現:

跟shutdownNow方法調用interruptWorkers方法不同的是,interruptIdleWorkers方法在遍歷線程池里的線程時,有一個w.tryLock()加鎖判斷,只有加鎖成功的線程才會被調用interrupt方法。那什么情況下才能被加鎖成功?什么情況下不能被加鎖成功呢?這就需要我們繼續回到線程執行的runWorker方法。

在上邊runWorker方法代碼的截圖中,我刻意將w.lock()和w.unlock()調用用紅框圈起。其實就是正運行在w.lock和w.unlock之間的線程將因為加鎖失敗,而不會被調用interrupt方法,換句話說,就是正在執行線程池里任務的線程不會被中斷。

不管是被調用了interrupt的線程還是沒被調用的線程,什么時候退出呢?,這就要看getTask方法的返回是否為null了。

在getTask里的if判斷(上文中getTask代碼截圖中上邊紅色方框的代碼)中,由於線程池被shutdown方法修改為SHUTDOWN狀態,SHUTDOWN大於等於SHUTDOWN成立沒問題,但是SHUTDOWN不在大於等於STOP狀態,所以只有隊列為空,getTask方法才會返回null,導致線程退出。

總結:

當我們調用線程池的shuwdown方法時,

如果線程正在執行線程池里的任務,即便任務處於阻塞狀態,線程也不會被中斷,而是繼續執行。

如果線程池阻塞等待從隊列里讀取任務,則會被喚醒,但是會繼續判斷隊列是否為空,如果不為空會繼續從隊列里讀取任務,為空則線程退出。

03 優雅的關閉線程池

有了上邊對兩個關閉線程池方法的了解,相信優雅安全關閉線程池將不再是問題。

我們知道,使用shutdownNow方法,可能會引起報錯,使用shutdown方法可能會導致線程關閉不了。

所以當我們使用shutdownNow方法關閉線程池時,一定要對任務里進行異常捕獲。

當我們使用shuwdown方法關閉線程池時,一定要確保任務里不會有永久阻塞等待的邏輯,否則線程池就關閉不了。

最后,一定要記得,shutdownNow和shuwdown調用完,線程池並不是立馬就關閉了,要想等待線程池關閉,還需調用awaitTermination方法來阻塞等待。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM