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

本文首發(fā)于http://www.itdecent.cn/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)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容