天天看點

大佬問我: notify()會立刻釋放鎖麼?

大佬問我: notify()會立刻釋放鎖麼?

我的内心戲: 肯定會啊! 這麼簡單的問題?

大佬問我: notify()會立刻釋放鎖麼?

image

聰明如我, 決定裝小白, 回答: 不會?

大佬: 很好, 小夥子基礎不錯!

我:

大佬問我: notify()會立刻釋放鎖麼?

image

大佬: 說說為什麼

我: ………………

大佬問我: notify()會立刻釋放鎖麼?

image

于是, 有了這篇文章!

問題的根本原來在于 “立刻”這個描述詞!

如果你和鹹魚君一樣懵逼, 不妨往下看!

技術大佬可以告辭了!!

接下來, 我們深入的分析分析wait和notify

前言

前面介紹了Synchronized關鍵詞的原理與優化分析,Synchronized的重要不言而喻, 而作為配合Synchronized使用的另外兩個關鍵字也顯得格外重要.

今天, 來聊聊配合Object基類的

  • wait()
  • notify()

這兩個方法的實作,為多線程協作提供了保證。

wait() & notify()

Object 類中的 wait&notify 這兩個方法,其實包括他們的重載方法一共有 5 個,而 Object 類中一共才 12 個方法,可見這 2 個方法的重要性。

我們先看看 JDK 中的定義:

public final native void notify();
           

其中有 3 個方法是 native 的,也就是由虛拟機本地的 c 代碼執行的。

ps: native 即 JNI,Java Native Interface,

Java平台提供的使用者和本地C代碼進行互操作的API

有 2 個 wait 重載方法最終還是調用了 wait(long)方法。

wait方法

wait是要釋放對象鎖,進入等待池。

既然是釋放對象鎖,那麼肯定是先要獲得鎖。

是以wait必須要寫在synchronized代碼塊中,否則會報異常。

notify方法

也需要寫在synchronized代碼塊中,

調用對象的這兩個方法也需要先獲得該對象的鎖.

notify,notifyAll, 喚醒等待該對象同步鎖的線程,并放入該對象的鎖池中.

對象的鎖池中線程可以去競争得到對象鎖,然後開始執行.

如果是通過notify來喚起的線程,

那進入wait的線程會被随機喚醒;

(注意: 實際上, hotspot是順序喚醒的!! 這是個重點! 有疑惑的點選傳送大佬問我: notify()是随機喚醒線程麼?

)

如果是通過notifyAll喚起的線程,

預設情況是最後進入的會先被喚起來,即LIFO的政策;

比較重要的是:

notify()或者notifyAll()調用時并不會真正釋放對象鎖, 必須等到synchronized方法或者文法塊執行完才真正釋放鎖.

舉個例子:

public void test()
{
    Object object = new Object();
    synchronized (object){
        object.notifyAll();
        while (true){
        }
    }
}
           

如上, 雖然調用了notifyAll, 但是緊接着進入了一個死循環。

這會導緻一直不能出臨界區, 一直不能釋放對象鎖。

是以,即使它把所有在等待池中的線程都喚醒放到了對象的鎖池中,

但是鎖池中的所有線程都不會運作,因為他們始終拿不到鎖。

案例分析

為了說明wait() 和notify()方法的功能,

我們舉個例子

public class WaitNotifyCase {
​
public static void main(String[] args) {
  final Object lock = new Object();
​
  new Thread(new Runnable() {
      @Override
      public void run() {
          System.out.println("線程 A 等待 獲得 鎖");
          synchronized (lock) {
              try {
                  System.out.println("線程 A 獲得 鎖");
                  TimeUnit.SECONDS.sleep(1);
                  System.out.println("線程 A 開始 執行 wait() ");
                  lock.wait();
                  System.out.println("線程 A 結束 執行 wait()");
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
          }
      }
  }).start();
​
  new Thread(new Runnable() {
      @Override
      public void run() {
          System.out.println("線程 B 等待 獲得 鎖");
          synchronized (lock) {
              System.out.println("線程 B 獲得 鎖");
              try {
                  TimeUnit.SECONDS.sleep(5);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              lock.notify();
              System.out.println("線程 B 執行 notify()");
          }
      }
  }).start();
}
}
           

執行結果:

線程 A 等待 獲得 鎖
線程 A 獲得 鎖
​
線程 B 等待 獲得 鎖
​
線程 A 開始 執行 wait()
​
線程 B 獲得 鎖
線程 B 執行 notify()
​
線程 A 結束 執行 wait()
           

使用時切記:必須由同一個lock對象調用wait、notify方法

  • 當線程A執行wait方法時,該線程會被挂起;
  • 當線程B執行notify方法時,會喚醒一個被挂起的線程A;

lock對象、線程A和線程B三者是一種什麼關系?

根據上面的案例,可以想象一個場景:

  • lock對象維護了一個等待隊列list;
  • 線程A中執行lock的wait方法,把線程A儲存到list中;
  • 線程B中執行lock的notify方法,從等待隊列中取出線程A繼續執行;

幾個疑問

問題一: 為何wait&notify必須要加synchronized鎖?

從實作上來說,這個synchronized鎖至關重要!

正因為這把鎖,才能讓整個wait/notify運轉起來.

當然我覺得其實通過其他的方式也可以實作類似的機制,

不過hotspot至少是完全依賴這把鎖來實作wait/notify的.

static void Sort(int [] array) {
    // synchronize this operation so that some other thread can't
    // manipulate the array while we are sorting it. This assumes that other
    // threads also synchronize their accesses to the array.
    synchronized(array) {
        // now sort elements in array
    }
}


           

synchronized代碼塊通過javap生成的位元組碼中包含monitorenter 和 monitorexit 指令

如下圖所示:

大佬問我: notify()會立刻釋放鎖麼?

image

執行monitorenter指令可以擷取對象的monitor,

而lock.wait()方法通過調用native方法wait(0)實作,其中接口注釋中有這麼一句:

The current thread must own this object's monitor.

表示線程執行 lock.wait() 方法時,必須持有該lock對象的monitor.

問題二: 為什麼wait方法可能抛出InterruptedException異常?

這個異常大家應該都知道,當我們調用了某個線程的interrupt方法時,對應的線程會抛出這個異常;

wait方法也不希望破壞這種規則,

是以就算目前線程因為wait一直在阻塞,當某個線程希望它起來繼續執行的時候,它還是得從阻塞态恢複過來;

而wait方法被喚醒起來的時候會去檢測這個狀态,當有線程interrupt了,它就會抛出這個異常從阻塞狀态恢複過來。

這裡有兩點要注意:

  1. 如果被interrupt的線程隻是建立了,并沒有start,那等他start之後進入wait态之後也是不能會恢複的;
  2. 如果被interrupt的線程已經start了,在進入wait之前,如果有線程調用了其interrupt方法,那這個wait等于什麼都沒做,會直接跳出來,不會阻塞;

問題三: notify執行之後立馬喚醒線程嗎?

其實hotspot裡真正的實作是: 退出同步塊的時候才會去真正喚醒對應的線程; 不過這個也是個預設政策,也可以改的,在notify之後立馬喚醒相關線程。

問題四: notifyAll是怎麼實作全喚起所有線程?

或許大家立馬就能想到一個for循環就搞定了,不過在JVM裡沒實作這麼簡單,而是借助了monitorexit.

上面提到了當某個線程從wait狀态恢複出來的時候,要先擷取鎖,然後再退出同步塊;

是以notifyAll的實作是調用notify的線程在退出其同步塊的時候喚醒起最後一個進入wait狀态的線程;

然後這個線程退出同步塊的時候繼續喚醒其倒數第二個進入wait狀态的線程,依次類推.

同樣這這是一個政策的問題,JVM裡提供了挨個直接喚醒線程的參數,不過很少使用, 這裡就不提了。

問題五: wait的線程是否會影響性能?

這是個大家比較關心的話題.

wait/nofity 是通過JVM裡的 park/unpark 機制來實作的,在Linux下這種機制又是通過pthread_cond_wait/pthread_cond_signal 來實作的;

是以當線程進入到wait狀态的時候其實是會放棄cpu的,也就是說這類線程是不會占用cpu資源。

作者:鹹魚君0808

連結:https://www.jianshu.com/p/ffc0c755fd8d

來源:簡書

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。