如何優雅的關閉Java執行緒池

java填坑路發表於2018-06-21

⾯試中經常會問到,建立⼀個執行緒池需要哪些引數、執行緒池的工作原理,卻很少會問到執行緒池如何安全關閉的。

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

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

優雅的關閉執行緒池

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

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

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

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

最後,⼀定要記得shutdownNow和shuwdown呼叫完,執行緒池並不是⽴刻就關閉了,要想等待執行緒池關閉,還需呼叫awaitTermination⽅法來阻塞等待。

END

歡迎工作一到五年的Java程式設計師朋友們加入Java架構開發:744677563

本群提供免費的學習指導 架構資料 以及免費的解答

不懂得問題都可以在本群提出來 之後還會有職業生涯規劃以及面試指導


相關文章