2012-06-27 78 views
9

我遇到下面的代碼的退出信號(在Android的錯誤?),我想知道,如果它不正是我想象的那樣:處理InterruptedException的等待

synchronized(sObject) { 
    mShouldExit = true; 
    sObject.notifyAll()  
    while (!mExited) { 
     try { 
      sObject.wait(); 
     } catch (InterruptedException ex) { 
      Thread.currentThread().interrupt(); 
     } 
    } 
} 

關於上下文:有另一個線程檢查mShouldExit(在sObject監視器內)並在此情況下退出。

這看起來不是一個正確的模式給我。如果發生中斷,它將再次設置中斷狀態,因此當它返回到sObject.wait()時,將出現另一個InterruptedException異常等等,因此它永遠不會進入真正的等待狀態(sObject.wait()),即它永遠不會釋放sObject監控。這可能會導致無限循環,因爲其他線程無法將mExiting設置爲true,因爲它永遠不會進入sObject的監視器。 (所以我認爲interrupt()調用是一個錯誤,不能在這裏使用。)我錯過了什麼嗎?

請注意,代碼片段是官方Android框架源代碼的一部分。

更新:實際上,情況更糟糕,因爲當你的GL渲染開始時,在Android中使用相同的模式。的GLSurfaceView.GLThread.surfaceCreated()官方源代碼:

public void surfaceCreated() { 
     synchronized(sGLThreadManager) { 
      if (LOG_THREADS) { 
       Log.i("GLThread", "surfaceCreated tid=" + getId()); 
      } 
      mHasSurface = true; 
      sGLThreadManager.notifyAll(); 
      while((mWaitingForSurface) && (!mExited)) { 
       try { 
        sGLThreadManager.wait(); 
       } catch (InterruptedException e) { 
        Thread.currentThread().interrupt(); 
       } 
      } 
     } 
    } 

您可以重現錯誤以類似的方式:請確保您的UI線程的中斷狀態標誌呢,然後添加您的GLSurfaceView並啓動GL渲染(通過setRenderer(...),但在某些設備上,請確保您的GLSurfaceView具有Visibility.VISIBLE狀態,否則將無法啓動渲染)。

如果你按照上面的步驟,你的UI線程將在一個無限循環結束了,因爲上面引述的代碼將繼續產生InterruptedException(由於wait()),因此GL線程將永遠無法設定mWaitingForSurface假。

根據我的測試,似乎這樣無限循環也將導致GC_CONCURRENT垃圾收集(或者,至少,在logcat中這樣的消息)的循環順序。有趣的是,有人對早期計算器一個未知的沒有準確定義的問題,這可能與: How to solve GC_concurrent freed?

難道就沒有可能,也許他的UI線程有其中斷標誌設置爲true,和他使用的一個GLSurfaceView他提到的地圖?只是一個假設,一個可能的場景。

+0

可能的重複[處理Java中的InterruptedException](http://stackoverflow.com/questions/3976344/handling-interruptedexception-in-java)和這一個:http://stackoverflow.com/questions/5915156/如何可以殺死線程無使用停止/ 5915306#5915306 – assylias

+1

它與鏈接的第一個主題無關(除了事實都與InterruptedException有關)。這是一個更復雜的問題,其他問題也完全是這樣。 (我知道你寫了「可能」,所以我只是告訴大家,它肯定不是)。編輯評論後添加的第二個更相關,它確切地證實了我懷疑上面的代碼是錯誤的,但既然它是官方的Android源代碼,那麼專家的明確回答將會很好。 –

+0

你是對的 - 事實上,這不是重複的事實,重置被中斷的狀態將立即等待退出。有人可能會看到這樣做的好理由... – assylias

回答

13

短版:該代碼是錯誤的,並會導致無限循環(我還有一個疑問,但可能取決於JVM實現)。設置中斷狀態是正確的,但它應該退出循環,最終使用Thread.isInterrupted()檢查相同的中斷狀態。

龍版爲普通讀者:

的問題是如何阻止當前從用戶或因其他一些應用程序邏輯執行的一些工作,以響應「取消」按鈕的線程。

最初,Java支持「停止」方法,即搶先停止線程。這種方法已被證明是不安全的,原因是沒有給停止的線程清理任何(簡單)方法,釋放資源,避免暴露部分修改的對象等等。

所以,Java的發展到「合作」主題「中斷」的系統。這個系統很簡單:一個線程正在運行,其他人調用「中斷」,線程上設置了一個標誌,線程負責檢查線程是否被中斷,並相應地執行。

所以,正確Thread.run(或Runnable.run,贖回等)的方法實現應該是這樣的:

public void run() { 
    while (!Thread.getCurrentThread().isInterrupted()) { 
    // Do your work here 
    // Eventually check isInterrupted again before long running computations 
    } 
    // clean up and return 
} 

這隻要是罰款,所有的線程執行的代碼在你的run方法裏面,你永遠不會調用長時間阻塞的東西......通常情況並非如此,如果你產生了一個線程的原因是因爲你有很長的一段時間。

該塊的Thread.sleep(millis)來最簡單的方法,它實際上是它的唯一的事情:它會阻止線程給定的時間量。

現在,如果中斷在線程在Thread.sleep(600000000)內時到達,沒有任何其他支持,則需要很多時間才能到達檢查isInterrupt的點。

甚至有線程永遠不會退出的情況。例如,您的線程正在計算某些內容並將結果發送到具有有限大小的BlockingQueue,您調用queue.put(myresult),它將阻塞,直到消費者釋放隊列中的一些空間,如果消費者已經中斷(或死亡或其他),該空間將永遠不會到達,該方法不會返回,.isInterrupted的檢查將永遠不會執行,您的線程被卡住。

爲了避免這種情況,即中斷線程所有(最)方法(應該)拋出InterruptedException。這個例外只是告訴你「我正在等待這個和那個,但同時線程被中斷,你應該儘快清理並退出」。

與所有的例外,除非你知道該怎麼做,你應該重新把它和希望有人比你在調用棧知道。

InterruptedExceptions甚至更糟,因爲當他們拋出的「中斷狀態」被清除。這意味着,只要捕捉和無視它們將導致線程通常不會停止:

public void run() { 
    while (!Thread.getCurrentThread().isInterrupted()) { 
    try { 
     Thread.sleep(1000); 
    } catch (InterruptedException e) { 
     // Nothing here 
    } 
    } 
} 

在這個例子中,如果睡眠()方法的過程中中斷的時候(這是時間99.9999999999%),它會拋出InterruptedException,清除中斷標誌,然後循環將繼續,因爲中斷標誌爲false,並且線程不會停止。

這就是爲什麼如果你正確地實現你的「while」,使用.isInterrupted,並且你真的需要捕獲InterruptedException,並且你沒有什麼特別的東西(比如清理,返回等等)來處理它,至少你可以做的是再次設置中斷標誌。

在您發佈的代碼的問題是,「而」專司mExited上isInterrupted依賴於決定什麼時候停止,不也。

while (!mExited && !Thread.getCurrentThread().isInterrupted()) { 

或中斷時,它可能會退出:

} catch (InterruptedException e) { 
    Thread.currentThread().interrupt(); 
    return; // supposing there is no cleanup or other stuff to be done 
} 

設置isInterrupted旗回真正的也是很重要的,如果你不控制線程。例如,如果你在一個正在某種線程池中執行的runnable中,或者在任何你不擁有和控制線程的任何方法內部(一個簡單的例子:一個servlet),你不知道是否中斷是針對「你」的(在servlet的情況下,客戶端關閉連接並且容器試圖阻止你爲其他請求釋放線程),或者如果它作爲一個整體針對線程(或系統)集裝箱正在關閉,停止一切)。

在這種情況下(這是99%的代碼),如果無法重新拋出InterruptedException(不幸的是,選中),則將堆棧傳播到線程池已被中斷的唯一方法,在返回之前將標誌設置回真。最終會產生更多的InterruptedException,直到可以正確反應的線程所有者(無論是Executor或任何其他線程池的jvm本身)(重用該線程,讓它死亡,System.exit(1)...)

大部分內容都在Java Concurrency in Practice的第7章中介紹過,這本書非常好,我向所有對計算機編程感興趣的人推薦,而不僅僅是Java,導致這些問題,並且解決方案在許多其他環境中是類似的,並且解釋寫得很好。

爲什麼Sun決定讓InterruptedException檢查,當大多數文檔建議無情地重新拋出它時,爲什麼他們決定在拋出異常時清除中斷標誌,當正確的事情是將其設置爲true時,大部分時間,仍然有爭議。

但是,如果.wait在檢查中斷標誌之前釋放鎖,它會從另一個線程打開一個小門以修改mExited布爾值。不幸的是,wait()方法是本地的,所以應該檢查特定JVM的源代碼。這並不會改變您發佈的代碼編碼不佳的事實。

+1

有人應該將它作爲一個錯誤報告發布給Google我猜。由於我發佈的代碼來自官方Android源代碼。檢查Grepcode顯示它也出現在最新的冰淇淋三明治中。 –

+0

是的,應該指出。我沒有使用Android的經驗,但如果需要,可以隨時在錯誤報告中使用我的答案。 –

+0

謝謝。他們有一個預定義的格式,需要一個工作示例來呈現錯誤等,所以不幸的是,在不久的將來我不會有時間這樣做。 –