Object對(duì)象方法之wait與notify

本文首發(fā)于http://www.reibang.com/p/18769b7dc46f

道生一录择,一生二拔莱,二生三,三生萬(wàn)物隘竭。
老子《道德經(jīng)》

Java是單繼承模型塘秦,有類似于老子的道德經(jīng)的哲學(xué),所有的類最終都會(huì)繼承自一個(gè)原始類动看,這個(gè)類就是Object尊剔。
Object對(duì)象中總共有11個(gè)可供protect或public方法。除去toString方法用于生成類的可讀化表示外菱皆,這些方法可以按其用途分為以下四類:

  • 類的表示及反射
    getClass
  • 類與Map的聯(lián)動(dòng)
    equals, hashCode
  • Java的同步抽象
    wait(三個(gè)重載), notify, notifyAll
  • Java中對(duì)象內(nèi)存表示及垃圾收集
    finalize, clone

本文結(jié)合Java語(yǔ)言規(guī)范 (Java 8) 介紹waitnotify方法的規(guī)范及其在同步中的應(yīng)用须误。

方法定義及規(guī)范

Object類中關(guān)于waitnotify方法的源碼如下:

    public final native void notify();

    public final native void notifyAll();

    public final native void wait(long timeout) throws InterruptedException;

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

  
    public final void wait() throws InterruptedException {
        wait(0);
    }

其中wait方法的兩個(gè)不同的重載都是wait(timeout)方法的輔助方法,值得一提的是方法wait(long timeout, int nanos)中主動(dòng)放棄了納秒級(jí)精度仇轻。由于常用的計(jì)算機(jī)系統(tǒng)都不是硬實(shí)時(shí)系統(tǒng)京痢,因此參數(shù)中的timeout都只能是一個(gè)粗略值。

Java語(yǔ)言規(guī)范中關(guān)于waitnotify的部分位于第17章拯田,這一章主要介紹Java的內(nèi)存模型历造,同步語(yǔ)義及抽象。

規(guī)范中關(guān)于wait方法的定義如下

假定線程t在對(duì)象m上執(zhí)行方法wait, nt執(zhí)行的lock次數(shù)。

  • 如果n == 0,即t還未獲得對(duì)象m的鎖吭产,那么會(huì)拋出IllegalMonitorStateException, 這也就是為什么waitnotify方法需要在sychronzied段中執(zhí)行侣监。
  • 如果t被中斷,那么會(huì)拋出InterruptedExceptiont的中斷狀態(tài)會(huì)被設(shè)置成false

否則

  1. 線程t會(huì)被加入對(duì)象m的等待集合臣淤,并且在對(duì)象m上執(zhí)行nunlock操作橄霉。
  2. t將不會(huì)執(zhí)行任何后續(xù)的指令,直到它從m的等待集合中移除邑蒋。
    線程會(huì)因?yàn)橐韵挛宸N原因從等待集合中移除姓蜂,并在之后的某個(gè)時(shí)間恢復(fù)運(yùn)行。
  • m.notify
  • m.notifyAll
  • t.interrupt
  • timeout超時(shí)
  • spurious wake-ups医吊。JVM允許此方法實(shí)現(xiàn)為隨機(jī)喚醒钱慢,因此處于wait狀態(tài)的線程可能沒(méi)有任何原因就被喚醒,這也是為什么使用wait方法時(shí)需要將其放在循環(huán)體中的原因卿堂。
  1. t在對(duì)象m上執(zhí)行nlock操作束莫。
  2. 如果t是由于調(diào)用t.interrupt從等待集合中移除,那么t的中斷狀態(tài)會(huì)置為false

規(guī)范中關(guān)于notifynotifyAll方法的定義如下:

線程t草描,對(duì)象m览绿,ntm上執(zhí)行的lock次數(shù)。

  • 如果n == 0, 則拋出IllegalMonitorStateException
  • 如果n > 0, 執(zhí)行notify會(huì)將m的等待集合中某個(gè)線程移除穗慕。
  • 如果n > 0, 執(zhí)行nofityAll會(huì)將m的等待集合中的所有線程移除饿敲。

規(guī)范中關(guān)于中斷的定義如下:
線程中斷有以下兩種方法來(lái)執(zhí)行
Thread.interrupt
ThraedGroup.interrupt
假設(shè)線程t執(zhí)行線程u的中斷方法u.interrupt(tu可能是同一個(gè)線程),這個(gè)調(diào)用會(huì)使得t的interruption狀態(tài)變成true.
如果u處于某個(gè)對(duì)象m的等待集合中,這將會(huì)使得uwait方法中恢復(fù)逛绵,并且拋出InterruptedExcetion
可以通過(guò)Thread.isInterrupted方法來(lái)判斷一個(gè)方法是否處于中斷狀態(tài)怀各。Thread.interrupted靜態(tài)方法可用于一個(gè)線程獲得它的中斷狀態(tài)并清空中斷狀態(tài)。

JVM字節(jié)碼

分別查看以下wait方法和notify方法對(duì)應(yīng)的字節(jié)碼术浪,結(jié)果如下:

  public static void main(String[] args) throws Exception {
    Object obj = new Object();
      synchronized (obj) {
        obj.wait();
      }
  }

-----
....
 11: monitorenter
 12: aload_1
 13: invokevirtual #3                  // Method java/lang/Object.wait:()V
 16: aload_2
 17: monitorexit
...
  public static void main(String[] args) throws Exception {
    Object obj = new Object();
      synchronized (obj) {
        obj.notify();
      }
  }
-----
...
  11: monitorenter
  12: aload_1
  13: invokevirtual #3                  // Method java/lang/Object.notify:()V
  16: aload_2
  17: monitorexit
...

從字節(jié)碼中可以看出渠啤,waitnotify功能由JVM的實(shí)現(xiàn),對(duì)應(yīng)的字節(jié)碼只是獲得相應(yīng)的監(jiān)視器鎖添吗,并執(zhí)行相應(yīng)的native方法沥曹。

代碼實(shí)例分析

  1. waitnotify方法一定需要放在sychronized
    分析以下代碼段
  public static void main(String[] args)  throws Exception{
    Object obj = new Object();
    obj.wait(); // throw java.lang.IllegalMonitorStateException
    obj.notify(); // throw java.lang.IllegalMonitorStateException
  }

根據(jù)JVM規(guī)范,若線程沒(méi)有獲得obj的監(jiān)視器鎖碟联,即規(guī)范中n == 0的情況下妓美,會(huì)拋出IllegalMonitorStateException
為什么需要在執(zhí)行waitnotify方法時(shí)先獲得對(duì)象鎖呢鲤孵?從規(guī)范中可以看出壶栋,waitnotify操作需要對(duì)對(duì)象的等待集合進(jìn)行更改,而這兩個(gè)更改本身就是競(jìng)態(tài)條件普监,因此需要同步贵试。

在JVM的wait方法的實(shí)現(xiàn)中琉兜,需要釋放已經(jīng)獲得對(duì)象監(jiān)視器鎖,從而允許執(zhí)行notify的代碼段獲得鎖并執(zhí)行毙玻。

  1. 事件有序
    interruptnotify事件必然以一定順序發(fā)生豌蟋,一個(gè)在wait中的線程,同時(shí)被另外兩個(gè)線程notifyinterupt時(shí)桑滩,線程要么被中斷梧疲,且通知被另外一個(gè)等待的線程獲取运准;要么線程先聽(tīng)到通知恢復(fù)執(zhí)行幌氮,未拋出InterruptException,線程的interuptted狀態(tài)變成true胁澳。
    可以由以下代碼驗(yàn)證:
public static void test() throws Exception {
    Object obj = new Object();

    CountDownLatch latch = new CountDownLatch(1);

    Thread t1 = new Thread(() -> {
      synchronized (obj) {
        try {
          obj.wait();
          System.out.println(Thread.currentThread().getName()
              + " be notify first");
          latch.countDown();
        } catch (InterruptedException e) {
          System.out.println(Thread.currentThread().getName()
              + " be interrupt first");
        }
      }
    });
    t1.start();

    new Thread(() -> {
      synchronized (obj) {
        try {
          obj.wait();
          if (latch.getCount() > 0) {
            System.out.println(Thread.currentThread().getName()
                + " count down latch");
            latch.countDown();
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }).start();

    new Thread(() -> {
      try {
        TimeUnit.MICROSECONDS.sleep(100);
      } catch (Exception ignore) {

      }
      t1.interrupt();
    }).start();

    new Thread(() -> {
      synchronized (obj) {
        try {
          TimeUnit.MICROSECONDS.sleep(100);
        } catch (Exception ignore) {

        }
        obj.notify();
      }
    }).start();

    try {
      latch.await(5, TimeUnit.SECONDS);
      System.out.println("latch can exit");
    } catch (Exception e) {
      // will never got here
      System.err.println("latch can not exit");
    }

    synchronized (obj) {
      obj.notifyAll();
    }
  }

  public static void main(String[] args) throws Exception {
    for (int i = 0; i < 1000; i++) {
      test();
    }
  }

使用wait和notify

  1. 生產(chǎn)者和消費(fèi)者問(wèn)題
    并發(fā)問(wèn)題的一個(gè)經(jīng)典場(chǎng)景是生產(chǎn)者和消費(fèi)者的問(wèn)題该互,有多個(gè)線程生產(chǎn),另外多個(gè)線程消費(fèi)韭畸。應(yīng)對(duì)這個(gè)問(wèn)題通常會(huì)使用阻塞隊(duì)列慢洋。
    假定隊(duì)列的接口是puttakeput用于生產(chǎn)陆盘,take用于消費(fèi)。非線程安全的queue用于存放元素败明。使用對(duì)象m用的waitnotify方法來(lái)實(shí)現(xiàn)同步隘马。

put的偽代碼如下

put (ele)  {
    sychronized(m) {
        while (queue.size() == capacity) {
           m.wait();  
        }
        queue.add(ele);
        m.notifyAll();
    }
} 

take的偽代碼如下

take () {
    sychronized(m) {
        while (queue.size() == 0) {
            m.wait();
        }
        queue.remove(0);
        m.notifyAll();
    }
}

需要注意的是這里使用的都是notifyAll。原因是notify只會(huì)隨機(jī)喚醒一個(gè)等級(jí)集合中的線程妻顶,如果有兩個(gè)生產(chǎn)者酸员,那么put中的notify喚醒的可能是另一個(gè)生產(chǎn)者,從而死鎖讳嘱。因此put方法中應(yīng)當(dāng)使用notifyAll幔嗦。同樣原因take方法中也應(yīng)當(dāng)使用notifyAll
另外一個(gè)需要注意的點(diǎn)是這里新建了一個(gè)對(duì)象m沥潭,并使用m的waitnotify方法來(lái)實(shí)現(xiàn)同步邀泉,既然Java中任何對(duì)象都繼承了Object對(duì)象,那么BlockQueue這個(gè)類本身也是有wait和notify方法的钝鸽,能否直接使用this.waitthis.notify并且在puttake方法上加synchronize呢汇恤?
答案是可以的。但是wait方法過(guò)程中會(huì)解除對(duì)象的監(jiān)視器鎖拔恰,從而會(huì)造成一些對(duì)synchronize的語(yǔ)義的干擾因谎。
比如下面的代碼

public class ReenterSync {
  public synchronized void reEnterSync() throws InterruptedException {
    System.out.println(Thread.currentThread().getName() + " enter sync");
    notify();
    System.out.println(Thread.currentThread().getName() + " leave sync");
  }

  public synchronized void syncWait()
      throws InterruptedException {
    System.out.println(Thread.currentThread().getName() + " enter sync");
    wait();
    System.out.println(Thread.currentThread().getName() + " leave sync");
  }

  public static void main(String[] args) {
    ReenterSync reenterSync = new ReenterSync();

    new Thread(() -> {
      try {
        reenterSync.syncWait();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }).start();

    new Thread(() -> {
      try {
        reenterSync.reEnterSync();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }).start();
  }
}
------------------
Thread-0 enter sync
Thread-1 enter sync
Thread-1 leave sync
Thread-0 leave sync

wait()方法會(huì)導(dǎo)致對(duì)象的監(jiān)視器被解鎖,從而導(dǎo)致有兩個(gè)線程同時(shí)進(jìn)入同一個(gè)對(duì)象的不同synchronize方法颜懊。這會(huì)造成不必要的困擾财岔。因此使用waitnotify來(lái)實(shí)現(xiàn)同步時(shí)通常會(huì)使用獨(dú)立的對(duì)象风皿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市匠璧,隨后出現(xiàn)的幾起案子桐款,更是在濱河造成了極大的恐慌,老刑警劉巖患朱,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鲁僚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡裁厅,警方通過(guò)查閱死者的電腦和手機(jī)冰沙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)执虹,“玉大人拓挥,你說(shuō)我怎么就攤上這事〈” “怎么了侥啤?”我有些...
    開(kāi)封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)茬故。 經(jīng)常有香客問(wèn)我盖灸,道長(zhǎng),這世上最難降的妖魔是什么磺芭? 我笑而不...
    開(kāi)封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任赁炎,我火速辦了婚禮,結(jié)果婚禮上钾腺,老公的妹妹穿的比我還像新娘徙垫。我一直安慰自己,他們只是感情好放棒,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布姻报。 她就那樣靜靜地躺著,像睡著了一般间螟。 火紅的嫁衣襯著肌膚如雪吴旋。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天厢破,我揣著相機(jī)與錄音邮府,去河邊找鬼。 笑死溉奕,一個(gè)胖子當(dāng)著我的面吹牛褂傀,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播加勤,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼仙辟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼同波!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起叠国,我...
    開(kāi)封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤未檩,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后粟焊,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體冤狡,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年项棠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了悲雳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡香追,死狀恐怖合瓢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情透典,我是刑警寧澤晴楔,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站峭咒,受9級(jí)特大地震影響税弃,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜凑队,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一则果、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧顽决,春花似錦、人聲如沸导匣。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)贡定。三九已至赋访,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間缓待,已是汗流浹背蚓耽。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留旋炒,地道東北人步悠。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像瘫镇,于是被迫代替她去往敵國(guó)和親鼎兽。 傳聞我的和親對(duì)象是個(gè)殘疾皇子答姥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容