java sleep和wait的區別的疑惑?


問題:

  sleep方法沒有釋放鎖:不讓出資源
  wait方法釋放了鎖:使得其他線程可以使用同步控制塊或者方法

  sleep不釋放鎖 線程是進入阻塞狀態還是就緒狀態?
  sleep是不是還占着CPU,是互斥還是同步?

 

作者:大寬寬
鏈接:https://www.zhihu.com/question/23328075/answer/665978836
來源:知乎

  首先說,雖然大家用Java Thread的api,但實際上Thread是OS提供的抽象和功能。這么理解會讓整個問題更清楚。千萬不要從類啊,靜態方法之類的角度去看待這個問題。這是Java設計上比較不可取的地方。

  一個Thread是指“是操作系統能夠進行運算調度的最小單位,以及相關資源的集合“。那么既然是可以調度的,線程本身就能“被調度”或者“暫停被調度”。所謂sleep是指讓線程“暫停被調度一段時間”,或者學術一點的詞叫“掛起”一段時間。整個sleep過程除了修改“掛起“狀態之外,不會動任何其他的”資源“。這些資源包括任何持有的任何形式的鎖。

  比如A線程如果先搶到一個鎖,然后B線程因為A線程搶到了就等着。接着A sleep了。B無論如何沒有任何機會去拿到這個鎖。你可以認為這樣就是你預期的,也可以認為這樣實際上因為A的實現,B的執行被卡住,浪費了CPU。因為你當你用了sleep的時候就意味着你想要讓當前線程不考慮其他線程的感受,只是自己暫時不干活而已。因此,對於問題:

sleep不釋放鎖 線程是進入阻塞狀態還是就緒狀態?

  答案是進入阻塞狀態,確切的說Thread在Java的狀態TIMED_WAITING(但這個狀態其實並沒那么重要,可以認為是java的內部細節,用戶不用太操心)。往下一層,在不同OS上底層的sleep的實現細節不太一樣。但是大體上就是掛起當前的線程,然后設置一個信號或者時鍾中斷到時候喚醒。sleep后的的Thread在被喚醒前是不會消耗任何CPU的(確切的說,大部分OS都會這么實現,除非某個OS的實現偷懶了)。這點上,wait對當前線程的效果差不多是一樣的,也會暫停調度,等着notify或者一個超時的時間。期間CPU也不會被消耗。

對於問題

wait方法釋放了鎖:使得其他線程可以使用同步控制塊或者方法?

這里需要多解釋一點。

  wait,和notify/notifyall是一套。他們是用來做多線程之間相互同步的工具的。這里不想死摳“同步”/“互斥”的字眼。只是當你用了wait/notify/notifyall之后,就意味着你主觀意圖上是想讓某個多個線程之間相互的“talk”,然后協商決定誰該執行那段代碼。這和sleep那種自顧自的方式完全不同。

  為啥要有wait/notify/notifyall呢?snychronized也可以協商啊。因為synchrnoized太簡單了,只能最基本的的同步的工作。但是有的時候需要根據特殊的條件來判定是不是應該進入幾端代碼。比如blocking queue,consume的代碼對於空的隊列就不能consume,得wait,直到有人produce;而produce代碼對於滿的隊列不能produce,也得wait,直到有人consume。wait就是解決這類問題的。wait要解決的關鍵點是,要判斷條件就要獲取鎖,而判斷條件本身又是能否獲取鎖的條件。雞和蛋的怪圈。

  於是Java是搞出了synchronized -- wait的結構,即“獲取鎖-判斷-判斷不滿足就釋放鎖-然后等通知重來”,用特定的代碼形式來實現“條件同步”:

synchronized { // 獲取鎖
  while (判定條件) wait(); // 如果條件不滿足就釋放鎖,並且等着
  // ... 要進入的代碼
  notify(); // 或者notifyAll,通知其他wait者可以重來
}

  這個寫法盡管能工作,但看起來蹩腳,還很容易出錯。如果不寫synchronized,就是運行時錯誤IllegalMonitorStateException;萬一忘記了while或者忘記notify,代碼正確性就可能有問題,並且連運行時錯誤都沒有。造成這些的也許是因為java本身沒把函數當作First Class Citizen造成的。如果換個語言,也許就可以這么寫:

lock.waitOnCond(判定條件, () -> {
 // ... 要進入的代碼
});

  這樣心智負擔會小,也更優雅。

  順便說下,wait的參數支持時間是為了允許編寫代碼避免永久等下去。比如可以實現一下插隊列等待最多30s,進不去就向上層報錯這種邏輯。但這還是解決wait自己場景的問題,和sleep的等待一段時間應用場景不同。wait要和synchronized搭配使用,而synchronized里有sleep就是定時炸彈了。

回到現實開發中,直接用sleep的機會很少的。大多數情況下,寫代碼都是希望代碼執行的越快越好,不會說故意sleep一下。如果有的話,可能是:

  • “模擬等待一個慢速操作“,這種經常用於測試里的mock。這時用sleep沒啥毛病。
  • 想等待一個慢速api的結果,比如發一個請求,等一會,看看結果來沒來。這種一般可以用Future定時等待的功能或者“join“等工具來處理。

  而對於wait/notify/notifyall,我建議任何初學者都只作為學習用途,不要放到生產代碼里。實際上wait/notify/notifally代表了一組最基礎的同步原語,極度容易出錯造成各種死鎖,活鎖等問題,極難測試和保證正確性(比如調整一行代碼的順序就正常,否則就死鎖;或者忘記寫了notify,結果wait永遠持續下去)。用鎖來做同步編寫代碼簡直就是萬惡之源。如果一定要用,那么請注意:

  1)自己對並發編程非常熟悉,已經是准專家或更高級別;
  2)有完備的測試工具和分析工具,以及大把的測試時間(數個月或者數年);
  3)編寫的是一套並發支持工具,而非直接用作業務邏輯控制。

  歸根到底線程+鎖這種並發模型是多種並發模型里最基礎的,最接近底層實現的,也是最不好用的。一般的並發性編碼,最好是尋求如Future/Promise,Actor,Fork & Join,協程等方式來做,即便是Java本身不支持,混搭一點點kotlin,clojure,scala,然后使用他們的並發模型是極好的。如果對各種並發模型感興趣,可以看看那本《七周七並發模型》。

順便擴展一下

  • Java的sleep api的形式並不太好,Thread.sleep其實是Thread.currentThread().sleep()的意思。見過很多初學者問“Thread.sleep到底sleep的是誰”這種問題。但是如果真的anotherThread.sleep()又不行。sleep實際上又只能暫停當前線程。這點上就能看出所謂OOP語言的表達能力令人郁悶之處。
  • 任何Object都可以wait/notify這個設計非常無語。Java希望把並發控制做到每一個Object里。其實這樣做即難以理解,又給每個Object增加了Overhead。想要明確使用Thread+鎖,那么就明確的表達鎖。JDK5之后,有了JUC,用明確的“XXXXLock”對象來實現同步。這個方式清楚明白的多;或者,如果想讓Object作為並發的隔離單元。Object之間並發安全,Object內部保證不會並發。這其實就是基於messaging的Actor模型。Java的做法夾在中間兩邊不討好。


免責聲明!

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



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