Java併發 -- 等待-通知機制
- 在《Java併發 – 死鎖》中,通過破壞 佔用且等待 條件來規避死鎖,核心程式碼如下
-
while (!allocator.apply(this, target)) {}
-
- 如果apply()操作的時間非常短,並且併發不大,該方案還能應付
- 一旦apply()操作比較耗時,或者併發比較大,該方案就不適用了
- 因為這可能需要迴圈上萬次才能獲得鎖,非常 消耗CPU
- 最好的方案: 等待-通知
- 當執行緒要求的條件不滿足,則執行緒 阻塞 自己,進入 等待 狀態
- 當執行緒要求的條件滿足後,通知等待的執行緒,重新開始執行
- 執行緒阻塞能夠避免因迴圈等待而消耗CPU的問題
- Java語言原生支援 等待-通知機制
- 執行緒 首先獲取互斥鎖 ,當執行緒要求的條件 不滿足 時, 釋放互斥鎖 ,進入等待狀態
- 當要求的條件滿足時,通知等待的執行緒, 重新獲取互斥鎖
等待-通知機制
Java語言原生支援的等待-通知機制: synchronized + wait + notify/notifyAll
wait
- 每個互斥鎖有兩個獨立的等待佇列 ,如上圖所示,等待佇列L和等待佇列R
- 左邊有一個 等待佇列 (等待佇列L), 在同一時刻,只允許一個執行緒進入synchronized保護的臨界區
- 當已經有一個執行緒進入synchronized保護的臨界區後,其他執行緒就只能進入等待佇列L進行等待
- 當一個執行緒 進入臨界區後 ,由於某些條件 不滿足 ,需要進入 等待 狀態,可以呼叫wait()方法
- 當呼叫wait()方法後,當前執行緒就會被 阻塞 ,並且進入到 右邊的等待佇列 (等待佇列R)
- 執行緒在進入等待佇列R的同時,會 釋放持有的互斥鎖 ,其他執行緒就有機會獲得鎖,並進入臨界區
- 關鍵點: sleep不會釋放互斥鎖
notify/notifyAll
- 當執行緒要求的條件滿足時,可以通過
notify/notifyAll
來通知等待的執行緒 - 當條件滿足時呼叫notify(),會通知等待佇列R( 將等待佇列L中第1個節點移到等待佇列R )中的執行緒,告知它 條件曾經滿足
- notify()只能保證在通知的時間點,條件是滿足的
- 而 被通知執行緒的執行時間點 與 通知時間點 基本上 不會重合 ,當執行緒執行的時候,條件很可能已經不滿足了
- 被通知的執行緒如果想要 重新執行 ,仍然需要先獲取到 互斥鎖
- 因為曾經獲取到的鎖在呼叫wait()時已經 釋放 了
- 關鍵點: 執行notify/notifyAll並不會釋放互斥鎖,在synchronized程式碼塊結束後才真正的釋放互斥鎖
編碼正規化
while (條件不滿足) { wait(); }
- 可以解決 條件曾經滿足 的問題
- 當wait()返回時,條件有可能已經改變了,需要重新檢驗條件是否滿足,如果不滿足,繼續wait()
notifyAll
- 儘量使用notifyAll()
- notify():會 隨機 地通知等待佇列R中的 一個 執行緒
- 隱含動作:先將等待佇列L的 第一個節點 移動到等待佇列R
- notifyAll():會通知等待佇列R中的 所有 執行緒
- 隱含動作:先將等待佇列L的 所有節點 移動到等待佇列R(待確定是否正確)
- notify()的風險: 可能導致某些執行緒永遠不會被通知到
- 假設有資源A、B、C、D,執行緒1~4都對應 同一個 互斥鎖L
- 執行緒1申請到了AB,執行緒2申請到了CD
- 此時執行緒3申請AB,會進入互斥鎖L的等待佇列L,執行緒4申請CD,也會進入互斥鎖L的等待佇列L
- 執行緒1歸還AB,通過notify()來通知互斥鎖L的等待佇列R中的執行緒,假設為執行緒4(先被移動到等待佇列R)
- 但執行緒4申請的是CD,不滿足條件,執行wait(),而真正該被喚醒的執行緒3就再也沒有機會被喚醒了
等待佇列
- wait/notify/notifyAll操作的等待佇列都是 互斥鎖的等待佇列
- 如果synchronized鎖定的是this,那麼對應的一定是this.wait()/this.notify()/this.notifyAll()
- 如果synchronized鎖定的是target,那麼對應的一定是target.wait()/target.notify()/target.notifyAll()
- 上面這3個方法能夠被呼叫的前提是 已經獲取了相應的互斥鎖 ,都必須在synchronized內部被呼叫
- 如果在synchronized外部呼叫,或者鎖定的是this,而呼叫的是target.wait(),JVM會丟擲IllegalMonitorStateException
public class IllegalMonitorStateExceptionTest { private Object lockA = new Object(); private Object lockB = new Object(); @Test public void test1() throws InterruptedException { // java.lang.IllegalMonitorStateException lockA.wait(); } @Test public void test2() throws InterruptedException { synchronized (lockA) { // java.lang.IllegalMonitorStateException lockB.wait(); } } }
轉賬例項
Allocator
public class Allocator { private static class Holder { private static Allocator allocator = new Allocator(); } public static Allocator getInstance() { return Holder.allocator; } private Allocator() { } private List<Object> als = new ArrayList<>(); // 一次性申請所有資源 public synchronized void apply(Object from, Object to) { // 程式設計正規化 while (als.contains(from) || als.contains(to)) { try { wait(); // this,單例 } catch (InterruptedException e) { e.printStackTrace(); } } als.add(from); als.add(to); } // 歸還資源 public synchronized void free(Object from, Object to) { als.remove(from); als.remove(to); notifyAll(); // this,單例 } }
Account
public class Account { // 必須是單例,因為要分配和釋放資源 private Allocator allocator = Allocator.getInstance(); // 賬戶餘額 private int balance; // 轉賬 public void transfer(Account target, int amt) { // 一次性申請轉出賬戶和轉入賬戶 allocator.apply(this, target); try { // 鎖定轉出賬戶 synchronized (this) { // 鎖定轉入賬戶 synchronized (target) { if (balance > amt) { balance -= amt; target.balance += amt; } } } } finally { allocator.free(this, target); } } }
轉載請註明出處:http://zhongmingmao.me/2019/04/22/java-concurrent-wait-notify/
訪問原文「 Java併發 -- 等待-通知機制 」獲取最佳閱讀體驗並參與討論