天天看點

詳細分析Java中斷機制

當我們點選某個防毒軟體的取消按鈕來停止清除病毒時,當我們在控制台敲入quit指令以結束某個背景服務時……都需要通過一個線程去取消另一個線程正在執行的任務。java沒有提供一種安全直接的方法來停止某個線程,但是java提供了中斷機制。

如果對java中斷沒有一個全面的了解,可能會誤以為被中斷的線程将立馬退出運作,但事實并非如此。中斷機制是如何工作的?捕獲或檢測到中斷後,是抛出

interruptedexception還是重設中斷狀态以及在方法中吞掉中斷狀态會有什麼後果?thread.stop與中斷相比又有哪些異同?什麼

情況下需要使用中斷?本文将從以上幾個方面進行描述。

java中斷機制是一種協作機制,也就是說通過中斷并不能直接終止另一個線程,而需要被中斷的線程自己進行中斷。這好比是家裡的父母叮囑在外的子女要注意身體,但子女是否注意身體,怎麼注意身體則完全取決于自己。

java中斷模型也是這麼簡單,每個線程對象裡都有一個boolean類型的辨別(不一定就要是thread類的字段,實際上也的确不是,這幾個方法最終

都是通過native方法來完成的),代表着是否有中斷請求(該請求可以來自所有線程,包括被中斷的線程本身)。例如,當線程t1想中斷線程t2,隻需要

線上程t1中将線程t2對象的中斷辨別置為true,然後線程2可以選擇在合适的時候處理該中斷請求,甚至可以不理會該請求,就像這個線程沒有被中斷一

樣。

java.lang.thread類提供了幾個方法來操作這個中斷狀态,這些方法包括:

public static boolean interrupted

測試目前線程是否已經中斷。線程的中斷狀态 由該方法清除。換句話說,如果連續兩次調用該方法,則第二次調用将傳回 false(在第一次調用已清除了其中斷狀态之後,且第二次調用檢驗完中斷狀态前,目前線程再次中斷的情況除外)。

public boolean isinterrupted()

測試線程是否已經中斷。線程的中斷狀态不受該方法的影響。

public void interrupt()

中斷線程。

其中,interrupt方法是唯一能将中斷狀态設定為true的方法。靜态方法interrupted會将目前線程的中斷狀态清除,但這個方法的命名極不直覺,很容易造成誤解,需要特别注意。

上面的例子中,線程t1通過調用interrupt方法将線程t2的中斷狀态置為true,t2可以在合适的時候調用interrupted或isinterrupted來檢測狀态并做相應的處理。

此外,類庫中的有些類的方法也可能會調用中斷,如futuretask中的cancel方法,如果傳入的參數為true,它将會在正在運作異步任務的線程

上調用interrupt方法,如果正在執行的異步任務中的代碼沒有對中斷做出響應,那麼cancel方法中的參數将不會起到什麼效果;又如

threadpoolexecutor中的shutdownnow方法會周遊線程池中的工作線程并調用線程的interrupt方法來中斷線程,是以如果

工作線程中正在執行的任務沒有對中斷做出響應,任務将一直執行直到正常結束。

既然java中斷機制隻是設定被中斷線程的中斷狀态,那麼被中斷線程該做些什麼?

處理時機

顯然,作為一種協作機制,不會強求被中斷線程一定要在某個點進行處理。實際上,被中斷線程隻需在合适的時候處理即可,如果沒有合适的時間點,甚至可以不處

理,這時候在任務處理層面,就跟沒有調用中斷方法一樣。“合适的時候”與線程正在處理的業務邏輯緊密相關,例如,每次疊代的時候,進入一個可能阻塞且無法

中斷的方法之前等,但多半不會出現在某個臨界區更新另一個對象狀态的時候,因為這可能會導緻對象處于不一緻狀态。

處理時機決定着程式的效率與中斷響應的靈敏性。頻繁的檢查中斷狀态可能會使程式執行效率下降,相反,檢查的較少可能使中斷請求得不到及時響應。如果發出中

斷請求之後,被中斷的線程繼續執行一段時間不會給系統帶來災難,那麼就可以将中斷處理放到友善檢查中斷,同時又能從一定程度上保證響應靈敏度的地方。當程

序的性能名額比較關鍵時,可能需要建立一個測試模型來分析最佳的中斷檢測點,以平衡性能和響應靈敏性。

處理方式

1、 中斷狀态的管理

一般說來,當可能阻塞的方法聲明中有抛出interruptedexception則暗示該方法是可中斷的,如blockingqueue#put、

blockingqueue#take、object#wait、thread#sleep等,如果程式捕獲到這些可中斷的阻塞方法抛出的

interruptedexception或檢測到中斷後,這些中斷資訊該如何處理?一般有以下兩個通用原則:

如果遇到的是可中斷的阻塞方法抛出interruptedexception,可以繼續向方法調用棧的上層抛出該異常,如果是檢測到中斷,則可清除中斷狀态并抛出interruptedexception,使目前方法也成為一個可中斷的方法。

若有時候不太友善在方法上抛出interruptedexception,比如要實作的某個接口中的方法簽名上沒有throws

interruptedexception,這時就可以捕獲可中斷方法的interruptedexception并通過

thread.currentthread.interrupt()來重新設定中斷狀态。如果是檢測并清除了中斷狀态,亦是如此。

一般的代碼中,尤其是作為一個基礎類庫時,絕不應當吞掉中斷,即捕獲到interruptedexception後在catch裡什麼也不做,清除中斷狀

态後又不重設中斷狀态也不抛出interruptedexception等。因為吞掉中斷狀态會導緻方法調用棧的上層得不到這些資訊。

當然,凡事總有例外的時候,當你完全清楚自己的方法會被誰調用,而調用者也不會因為中斷被吞掉了而遇到麻煩,就可以這麼做。

總得來說,就是要讓方法調用棧的上層獲知中斷的發生。假設你寫了一個類庫,類庫裡有個方法amethod,在amethod中檢測并清除了中斷狀态,而沒

有抛出interruptedexception,作為amethod的使用者來說,他并不知道裡面的細節,如果使用者在調用amethod後也要使用中斷來

做些事情,那麼在調用amethod之後他将永遠也檢測不到中斷了,因為中斷資訊已經被amethod清除掉了。如果作為使用者,遇到這樣有問題的類庫,又

不能修改代碼,那該怎麼處理?隻好在自己的類裡設定一個自己的中斷狀态,在調用interrupt方法的時候,同時設定該狀态,這實在是無路可走時才使用

的方法。

2、 中斷的響應

程式裡發現中斷後該怎麼響應?這就得視實際情況而定了。有些程式可能一檢測到中斷就立馬将線程終止,有些可能是退出目前執行的任務,繼續執行下一個任

務……作為一種協作機制,這要與中斷方協商好,當調用interrupt會發生些什麼都是事先知道的,如做一些事務復原操作,一些清理工作,一些補償操作

等。若不确定調用某個線程的interrupt後該線程會做出什麼樣的響應,那就不應當中斷該線程。

thread.stop方法已經不推薦使用了。而在某些方面thread.stop與中斷機制有着相似之處。如當線程在等待内置鎖或io時,stop跟

interrupt一樣,不會中止這些操作;當catch住stop導緻的異常時,程式也可以繼續執行,雖然stop本意是要停止線程,這麼做會讓程式行

為變得更加混亂。

那麼它們的差別在哪裡?最重要的就是中斷需要程式自己去檢測然後做相應的處理,而thread.stop會直接在代碼執行過程中抛出threaddeath錯誤,這是一個java.lang.error的子類。

在繼續之前,先來看個小例子:

<code>01</code>

<code>package</code> <code>com.ticmy.interrupt;</code>

<code>02</code>

<code>import</code> <code>java.util.arrays;</code>

<code>03</code>

<code>import</code> <code>java.util.random;</code>

<code>04</code>

<code>import</code> <code>java.util.concurrent.timeunit;</code>

<code>05</code>

<code>public</code> <code>class</code> <code>teststop {</code>

<code>06</code>

<code>    </code><code>private</code> <code>static</code> <code>final</code> <code>int</code><code>[] array =</code><code>new</code> <code>int</code><code>[</code><code>80000</code><code>];</code>

<code>07</code>

<code>    </code><code>private</code> <code>static</code> <code>final</code> <code>thread t =</code><code>new</code> <code>thread() {</code>

<code>08</code>

<code>        </code><code>public</code> <code>void</code> <code>run() {</code>

<code>09</code>

<code>            </code><code>try</code> <code>{</code>

<code>10</code>

<code>                </code><code>system.out.println(sort(array));</code>

<code>11</code>

<code>            </code><code>}</code><code>catch</code> <code>(error err) {</code>

<code>12</code>

<code>                </code><code>err.printstacktrace();</code>

<code>13</code>

<code>            </code><code>}</code>

<code>14</code>

<code>            </code><code>system.out.println(</code><code>"in thread t"</code><code>);</code>

<code>15</code>

<code>        </code><code>}</code>

<code>16</code>

<code>    </code><code>};</code>

<code>17</code>

<code>    </code> 

<code>18</code>

<code>    </code><code>static</code> <code>{</code>

<code>19</code>

<code>        </code><code>random random =</code><code>new</code> <code>random();</code>

<code>20</code>

<code>        </code><code>for</code><code>(</code><code>int</code> <code>i =</code><code>0</code><code>; i &lt; array.length; i++) {</code>

<code>21</code>

<code>            </code><code>array[i] = random.nextint(i +</code><code>1</code><code>);</code>

<code>22</code>

<code>23</code>

<code>    </code><code>}</code>

<code>24</code>

<code>25</code>

<code>    </code><code>private</code> <code>static</code> <code>int</code> <code>sort(</code><code>int</code><code>[] array) {</code>

<code>26</code>

<code>        </code><code>for</code> <code>(</code><code>int</code> <code>i =</code><code>0</code><code>; i &lt; array.length-</code><code>1</code><code>; i++){</code>

<code>27</code>

<code>            </code><code>for</code><code>(</code><code>int</code> <code>j =</code><code>0</code> <code>;j &lt; array.length - i -</code><code>1</code><code>; j++){</code>

<code>28</code>

<code>                </code><code>if</code><code>(array[j] &lt; array[j +</code><code>1</code><code>]){</code>

<code>29</code>

<code>                    </code><code>int</code> <code>temp = array[j];</code>

<code>30</code>

<code>                    </code><code>array[j] = array[j +</code><code>1</code><code>];</code>

<code>31</code>

<code>                    </code><code>array[j +</code><code>1</code><code>] = temp;</code>

<code>32</code>

<code>                </code><code>}</code>

<code>33</code>

<code>34</code>

<code>35</code>

<code>        </code><code>return</code> <code>array[</code><code>0</code><code>];</code>

<code>36</code>

<code>37</code>

<code>38</code>

<code>    </code><code>public</code> <code>static</code> <code>void</code> <code>main(string[] args)</code><code>throws</code> <code>exception {</code>

<code>39</code>

<code>        </code><code>t.start();</code>

<code>40</code>

<code>        </code><code>timeunit.seconds.sleep(</code><code>1</code><code>);</code>

<code>41</code>

<code>        </code><code>system.out.println(</code><code>"go to stop thread t"</code><code>);</code>

<code>42</code>

<code>        </code><code>t.stop();</code>

<code>43</code>

<code>        </code><code>system.out.println(</code><code>"finish main"</code><code>);</code>

<code>44</code>

<code>45</code>

<code>}</code>

這個例子很簡單,線程t裡面做了一個非常耗時的排序操作,排序方法中,隻有簡單的加、減、指派、比較等操作,一個可能的執行結果如下:

這裡sort方法是個非常耗時的操作,也就是說主線程休眠一秒鐘後調用stop的時候,線程t還在執行sort方法。就是這樣一個簡單的方法,也會抛出錯誤!換一句話說,調用stop後,大部分java位元組碼都有可能抛出錯誤,哪怕是簡單的加法!

如果線程目前正持有鎖,stop之後則會釋放該鎖。由于此錯誤可能出現在很多地方,那麼這就讓程式設計人員防不勝防,極易造成對象狀态的不一緻。例如,對象

obj中存放着一個範圍值:最小值low,最大值high,且low不得大于high,這種關系由鎖lock保護,以避免并發時産生競态條件而導緻該關系

失效。假設目前low值是5,high值是10,當線程t擷取lock後,将low值更新為了15,此時被stop了,真是糟糕,如果沒有捕獲住stop

導緻的error,low的值就為15,high還是10,這導緻它們之間的小于關系得不到保證,也就是對象狀态被破壞了!如果在給low指派的時候

catch住stop導緻的error則可能使後面high變量的指派繼續,但是誰也不知道error會在哪條語句抛出,如果對象狀态之間的關系更複雜

呢?這種方式幾乎是無法維護的,太複雜了!如果是中斷操作,它決計不會在執行low指派的時候抛出錯誤,這樣程式對于對象狀态一緻性就是可控的。

正是因為可能導緻對象狀态不一緻,stop才被禁用。

通常,中斷的使用場景有以下幾個:

點選某個桌面應用中的取消按鈕時;

某個操作超過了一定的執行時間限制需要中止時;

多個線程做相同的事情,隻要一個線程成功其它線程都可以取消時;

一組線程中的一個或多個出現錯誤導緻整組都無法繼續時;

當一個應用或服務需要停止時。

下面來看一個具體的例子。這個例子裡,本打算采用gui形式,但考慮到gui代碼會使程式複雜化,就使用控制台來模拟下核心的邏輯。這裡建立了一個磁盤文

件掃描的任務,掃描某個目錄下的所有檔案并将檔案路徑列印到控制台,掃描的過程可能會很長。若需要中止該任務,隻需在控制台鍵入quit并回車即可。

<code>import</code> <code>java.io.bufferedreader;</code>

<code>import</code> <code>java.io.file;</code>

<code>import</code> <code>java.io.inputstreamreader;</code>

<code>public</code> <code>class</code> <code>filescanner {</code>

<code>    </code><code>private</code> <code>static</code> <code>void</code> <code>listfile(file f)</code><code>throws</code> <code>interruptedexception {</code>

<code>        </code><code>if</code><code>(f ==</code><code>null</code><code>) {</code>

<code>            </code><code>throw</code> <code>new</code> <code>illegalargumentexception();</code>

<code>        </code><code>if</code><code>(f.isfile()) {</code>

<code>            </code><code>system.out.println(f);</code>

<code>            </code><code>return</code><code>;</code>

<code>        </code><code>file[] allfiles = f.listfiles();</code>

<code>        </code><code>if</code><code>(thread.interrupted()) {</code>

<code>            </code><code>throw</code> <code>new</code> <code>interruptedexception(</code><code>"檔案掃描任務被中斷"</code><code>);</code>

<code>        </code><code>for</code><code>(file file : allfiles) {</code>

<code>            </code><code>//還可以将中斷檢測放到這裡</code>

<code>            </code><code>listfile(file);</code>

<code>    </code><code>public</code> <code>static</code> <code>string readfromconsole() {</code>

<code>        </code><code>bufferedreader reader =</code><code>new</code> <code>bufferedreader(</code><code>new</code> <code>inputstreamreader(system.in));</code>

<code>        </code><code>try</code> <code>{</code>

<code>            </code><code>return</code> <code>reader.readline();</code>

<code>        </code><code>}</code><code>catch</code> <code>(exception e) {</code>

<code>            </code><code>e.printstacktrace();</code>

<code>            </code><code>return</code> <code>""</code><code>;</code>

<code>        </code><code>final</code> <code>thread fileiteratorthread =</code><code>new</code> <code>thread() {</code>

<code>            </code><code>public</code> <code>void</code> <code>run() {</code>

<code>                </code><code>try</code> <code>{</code>

<code>                    </code><code>listfile(</code><code>new</code> <code>file(</code><code>"c:\\"</code><code>));</code>

<code>                </code><code>}</code><code>catch</code> <code>(interruptedexception e) {</code>

<code>                    </code><code>e.printstacktrace();</code>

<code>        </code><code>};</code>

<code>        </code><code>new</code> <code>thread() {</code>

<code>46</code>

<code>47</code>

<code>                </code><code>while</code><code>(</code><code>true</code><code>) {</code>

<code>48</code>

<code>                    </code><code>if</code><code>(</code><code>"quit"</code><code>.equalsignorecase(readfromconsole())) {</code>

<code>49</code>

<code>                        </code><code>if</code><code>(fileiteratorthread.isalive()) {</code>

<code>50</code>

<code>                            </code><code>fileiteratorthread.interrupt();</code>

<code>51</code>

<code>                            </code><code>return</code><code>;</code>

<code>52</code>

<code>                        </code><code>}</code>

<code>53</code>

<code>                    </code><code>}</code><code>else</code> <code>{</code>

<code>54</code>

<code>                        </code><code>system.out.println(</code><code>"輸入quit退出檔案掃描"</code><code>);</code>

<code>55</code>

<code>                    </code><code>}</code>

<code>56</code>

<code>57</code>

<code>58</code>

<code>        </code><code>}.start();</code>

<code>59</code>

<code>        </code><code>fileiteratorthread.start();</code>

<code>60</code>

<code>61</code>

在掃描檔案的過程中,對于中斷的檢測這裡采用的政策是,如果碰到的是檔案就不檢測中斷,是目錄才檢測中斷,因為檔案可能是非常多的,每次遇到檔案都檢測一

次會降低程式執行效率。此外,在fileiteratorthread線程中,僅是捕獲了interruptedexception,沒有重設中斷狀态也

沒有繼續抛出異常,因為我非常清楚它的使用環境,run方法的調用棧上層已經沒有可能需要檢測中斷狀态的方法了。

在這個程式中,輸入quit完全可以執行system.exit(0)操作來退出程式,但正如前面提到的,這是個gui程式核心邏輯的模拟,在gui中,執行system.exit(0)會使得整個程式退出。

《java concurrency in practice》

《concurrent programming in java design principles and patterns》

<a href="http://docs.oracle.com/javase/1.4.2/docs/guide/misc/threadprimitivedeprecation.html">http://docs.oracle.com/javase/1.4.2/docs/guide/misc/threadprimitivedeprecation.html</a>