如何優雅的關閉Java線程池

如何優雅的關閉Java線程池



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


也正是因為⼤家不是很關注這塊,即便是⼯作三四年的⼈,也會有因為線程池關閉不合理,導致應用⽆法正常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⽅法的源碼:


如何優雅的關閉Java線程池



在shutdownNow⽅法裡,重要的三句代碼我⽤紅⾊數字標出來了。第⼀句就是原⼦性的修改線程池的狀態為STOP狀態(比較簡單,我就不貼代碼了) ,第三句是將隊列裡還沒有執⾏的任務放到列表裡,返回給調用方,第⼆句是遍歷線程池⾥的所有⼯作線程,然後調用線程的interrupt方法。如下圖:


如何優雅的關閉Java線程池



以上就是shutdownNow⽅法的執⾏邏輯:將線程池狀態修改為STOP,然後調⽤線程池⾥的所有線程的interrupt⽅法。


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


如何優雅的關閉Java線程池



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

⾏,則不受影響,繼續執⾏完這個任務。


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


如何優雅的關閉Java線程池


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


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

總結當我們調用線程池的shutdownNow時,如果線程正在getTask方法中執⾏,則會通過for循環進入到if語句,於是getTask 返回null,從而線程退出。不管線程池⾥是否有未完成的任務。如果線程因為執行提交到線程池裡的任務而處於阻塞狀態,則會導致報錯。(如果任務裡沒有捕獲InterruptedException異常),否則線程會執行完當前任務,然後通過getTask方法返回為null來退出。



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


如何優雅的關閉Java線程池



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


如何優雅的關閉Java線程池



跟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方法時,如果線程正在執行線程池裡的任務,即便任務處於阻塞狀態,線程也不會被中斷,⽽是繼續執行。如果線程池阻塞等待從隊列⾥讀取任務,則會被喚醒,但是會繼續判斷隊列是否為空,若不為空,則會繼續從隊列裡讀取任務,若為空則線程退出。


優雅的關閉線程池




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


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


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


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


最後,⼀定要記得shutdownNow和shuwdown調用完,線程池並不是⽴刻就關閉了,要想等待線程池關閉,還需調用awaitTermination⽅法來阻塞等待


分享到:


相關文章: