你知道wait/notify的這些知識點嗎?

在Java的Object類中有2個咱們不怎麼經常使用(框架中用的更多)的方法:wait()與notify()或notfiyAll(),這兩個方法主要用於多線程間的協同處理,即控制線程之間的等待、通知、切換及喚醒。html

首先了解下線程有哪幾種狀態,Java的Thread.State中定義了線程的6種狀態,分別以下:java

  1. NEW 未啓動的,不會出如今dump文件中(能夠經過jstack命令查看線程堆棧)
  2. RUNNABLE 正在JVM中執行的
  3. BLOCKED 被阻塞,等待獲取監視器鎖進入synchronized代碼塊或者在調用Object.wait以後從新進入synchronized代碼塊
  4. WAITING 無限期等待另外一個線程執行特定動做後喚醒它,也就是調用Object.wait後會等待擁有同一個監視器鎖的線程調用notify/notifyAll來進行喚醒
  5. TIMED_WAITING 有時限的等待另外一個線程執行特定動做
  6. TERMINATED 已經完成了執行

從操做系統層面上來說,一個進程從建立到消亡期間,最多見的進程狀態有如下幾種 新建態 : 從程序映像到進程映像的轉變,尚未加入到就緒隊列中 就緒態 : 進程運行已萬事俱備,正等待調度執行 運行態 : 進程指令正在被執行 阻塞態 : 進程正在等待一個時間操做完成,例如I/O操做 完成態 : 進程運行結束,它的資源已經被釋放,供其餘活動進程使用linux

接下來咱們分析下面的代碼git

public class WaitNotify {

  public static void main(String[] args) {

    Object lock = new Object();
    
    // thread1
    new Thread(() -> {

        System.out.println("thread1 is ready");

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {

        }
        synchronized (lock) {
            lock.notify();
            System.out.println("thread1 is notify,but not exit synchronized");
            System.out.println("thread1 is exit synchronized");
        }


    }).start();

    // thread2
    new Thread(() -> {
        System.out.println("thread2 is ready");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {

        }
        synchronized (lock) {
            try {
                System.out.println("thread2 is waiting");
                lock.wait();
                System.out.println("thread2 is awake");
            } catch (InterruptedException e) {
            }
        }
    }).start();
  }
}
複製代碼

上面的代碼運行結果以下github

thread1 is ready
thread2 is ready
thread2 is waiting
thread1 is notify,but not exit synchronized
thread 1 is exit synchronized
thread2 is awake
複製代碼

看到這裏你會發現平平無奇,好像也沒什麼特殊的事情,可是若是深刻分析下,就會發現如下的問題api

  1. 爲什麼調用wait或者notify必定要加synchronized,不加行不行?
  2. thread2中調用了wait後,當前線程還未退出同步代碼塊,其餘線程(thread1)能進入同步塊嗎?
  3. 爲什麼調用wait()有可能拋出InterruptedException異常
  4. 調用notify/notifyAll後等待中的線程會馬上喚醒嗎?
  5. 調用notify/notifyAll是隨機從等待線程隊列中取一個或者按某種規律取一個來執行?
  6. wait的線程是否會影響系統性能?

針對上面的問題,咱們逐個分析下bash

爲什麼調用wait或者notify必定要加synchronized,不加行不行?

若是你不加,你會獲得下面的異常多線程

Exception in thread "main" java.lang.IllegalMonitorStateException
複製代碼

JVM源代碼中首先會檢查當前線程是否持有鎖,若是沒有持有則拋出異常oracle

其次爲何要加,也有比較普遍的討論,首先wait/notify是爲了線程間通訊的,爲了這個通訊過程不被打斷,須要保證wait/notify這個總體代碼塊的原子性,因此須要經過synchronized來加鎖。框架

thread2中調用了wait後,當前線程還未退出同步代碼塊,其餘線程(thread1)能進入同步塊嗎?

wait在處理過程當中會臨時釋放同步鎖(若是不釋放其餘線程沒有機會搶),不過須要注意的是當其餘線程調用notify喚起這個線程的時候,在wait方法退出以前會從新獲取這把鎖,只有獲取了這把鎖纔會繼續執行,這也和咱們的結果相符合,輸出了thread2 is awake, 其實想一想也容易理解,synchronized的代碼其實是被被monitorenter和monitorexit包圍起來的。當咱們調用wait的時候,會釋放鎖,調用monitorexit的時候也會釋放鎖,那麼當thread2被喚醒的時候必然從新獲取到了鎖(objectMonitor::enter)。

其實從jdk源代碼的ObjectMonitor::wait方法能夠一窺究竟,首先會放棄已經搶到的鎖(exit(self)),而放棄鎖的前提是獲取到鎖

而在notify方法中會選取一個線程得到cpu執行權,在去競爭鎖,若是沒有競爭到則會進入休眠。

若是調用的wait(200)這種代碼,那麼會在200ms後將線程從waiting set中移除並容許其從新競爭鎖,須要注意的是notify方法並不會釋放所持有的monitor

爲什麼調用wait()有可能拋出InterruptedException異常

當咱們調用了某個線程的interrupt方法,對應的線程會拋出這個異常,wait方法也不但願去破壞這種規則,所以就算當前線程由於wait一直在阻塞。當某個線程但願它起來繼續執行的時候,它仍是得從阻塞態恢復過來,所以wait方法被喚醒起來的時候會去檢測這個狀態,當有線程interrupt了它的時候,它就會拋出這個異常從阻塞狀態恢復過來。

這裏有兩點要注意:

  1. 若是被interrupt的線程只是建立了,並無start,那等他start以後進入wait態以後也是不能會恢復的
  2. 若是被interrupt的線程已經start了,在進入wait以前,若是有線程調用了其interrupt方法,那這個wait等於什麼都沒作,會直接跳出來,不會阻塞,下面的代碼演示了這種狀況
Thread thread2 = new Thread(() -> {
    System.out.println("thread2 is ready");

    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {

    }
    synchronized (lock) {
        try {
            System.out.println("thread2 is waiting");
            lock.wait();
            System.out.println("thread2 is awake");
        } catch (InterruptedException e) {
            System.out.println(e);
        }
    }
});

  // main
  thread2.start();

  Thread.sleep(1000);
  thread2.interrupt();

複製代碼

上面的代碼輸出結果以下

thread2 is ready
thread2 is waiting
java.lang.InterruptedException
複製代碼

調用notify/notifyAll後等待中的線程會馬上喚醒嗎?

hotspot真正的實現是退出同步代碼塊的時候纔會去真正喚醒對應的線程,不過這個也是個默認策略,也能夠改的,在notify以後立馬喚醒相關線程。 這個也可從jdk源代碼的objectMonitor類objectMonitor::notify方法中看到.在調用notify時的默認策略是Policy == 2(這個值是源碼中的初值,能夠經過-XX:SyncKnobs來設置)

其實對於Policy(一、二、三、4)都是將objectMonitor的ObjectWaiter集合中取出一個等待線程,放入到_EntryList(blocked線程集合,能夠參與下次搶鎖),只是放入_EntryList的策略不同,體現爲喚醒wait線程的規則不同。

對於默認策略notify在將一個等待線程放入阻塞線程集合以後就退出,由於同步塊尚未執行完monitorexit,鎖其實還未釋放,因此在打印出「thread1 is exit synchronized!」的時候,thread2線程仍是blocked狀態(由於thread1尚未退出同步塊)。

這裏能夠發現,對於不在Policy中的狀況,會直接將一個ObjectWaiter進行unpark喚醒操做,可是被喚醒的線程是否當即獲取到了鎖呢?答案是否認的。

調用notify/notifyAll是隨機從等待線程隊列中取一個或者按某種規律取一個來執行?

咱們本身實現可能一個for循環就搞定了,不過在jvm裏實現沒這麼簡單,而是藉助了monitor_exit,上面我提到了當某個線程從wait狀態恢復出來的時候,要先獲取鎖,而後再退出同步塊,因此notifyAll的實現是調用notify的線程在退出其同步塊的時候喚醒起最後一個進入wait狀態的線程,而後這個線程退出同步塊的時候繼續喚醒其倒數第二個進入wait狀態的線程,依次類推,一樣這是一個策略的問題,jvm裏提供了挨個直接喚醒線程的參數,這裏要分狀況:

  1. 若是是經過notify來喚起的線程,那先進入wait的線程會先被喚起來
  2. 若是是經過nootifyAll喚起的線程,默認狀況是最後進入的會先被喚起來,即LIFO的策略

wait的線程是否會影響系統性能?

這個或許是你們比較關心的話題,由於關乎系統性能問題,wait/nofity是經過jvm裏的park/unpark機制來實現的,在linux下這種機制又是經過pthread_cond_wait/pthread_cond_signal來玩的,所以當線程進入到wait狀態的時候實際上是會放棄cpu的,也就是說這類線程是不會佔用cpu資源,也不會影響系統加載。

什麼是監視器(monitor)

Java中每個對象均可以成爲一個監視器(Monitor), 該Monitor由一個鎖(lock), 一個等待隊列(waiting queue ), 一個入口隊列( entry queue)組成. 對於一個對象的方法, 若是沒有synchonized關鍵字, 該方法能夠被任意數量的線程,在任意時刻調用。 對於添加了synchronized關鍵字的方法,任意時刻只能被惟一的一個得到了對象實例鎖的線程調用。

進入區(Entry Set): 表示線程經過 synchronized要求得到對象鎖,若是獲取到了,則成爲擁有者,若是沒有獲取到在在進入區等待,直到其餘線程釋放鎖以後再去競爭(誰獲取到則根據)

擁有者(Owner): 表示線程獲取到了對象鎖,能夠執行synchronized包圍的代碼了

等待區(Wait Set): 表示線程調用了wait方法,此時釋放了持有的對象鎖,等待被喚醒(誰被喚醒取得監視器鎖由jvm決定)

關於sleep

它是一個靜態方法,通常的調用方式是Thread.sleep(2000),表示讓當前線程休眠2000ms,並不會讓出監視器,這一點須要注意。

你的關注是對我最大的支持。

相關文章
相關標籤/搜索