Object对象方法之wait与notify

Object对象方法之wait与notify

本文首发于https://www.jianshu.com/p/18769b7dc46f

道生一,一生二,二生三,三生万物。
老子《道德经》

Java是单继承模型,有类似于老子的道德经的哲学,所有的类最终都会继承自一个原始类,这个类就是Object
Object对象中总共有11个可供protect或public方法。除去toString方法用于生成类的可读化表示外,这些方法可以按其用途分为以下四类:

  • 类的表示及反射
    getClass
  • 类与Map的联动
    equals, hashCode
  • Java的同步抽象
    wait(三个重载), notify, notifyAll
  • Java中对象内存表示及垃圾收集
    finalize, clone

本文结合Java语言规范 (Java 8) 介绍waitnotify方法的规范及其在同步中的应用。

方法定义及规范

Object类中关于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方法的两个不同的重载都是wait(timeout)方法的辅助方法,值得一提的是方法wait(long timeout, int nanos)中主动放弃了纳秒级精度。由于常用的计算机系统都不是硬实时系统,因此参数中的timeout都只能是一个粗略值。

Java语言规范中关于waitnotify的部分位于第17章,这一章主要介绍Java的内存模型,同步语义及抽象。

规范中关于wait方法的定义如下

假定线程t在对象m上执行方法wait, nt执行的lock次数。
+ 如果n == 0,即t还未获得对象m的锁,那么会抛出IllegalMonitorStateException, 这也就是为什么waitnotify方法需要在sychronzied段中执行。
+ 如果t被中断,那么会抛出InterruptedExceptiont的中断状态会被设置成false

否则
1. 线程t会被加入对象m的等待集合,并且在对象m上执行nunlock操作。
2. t将不会执行任何后续的指令,直到它从m的等待集合中移除。
线程会因为以下五种原因从等待集合中移除,并在之后的某个时间恢复运行。

  • m.notify
  • m.notifyAll
  • t.interrupt
  • timeout超时
  • spurious wake-ups。JVM允许此方法实现为随机唤醒,因此处于wait状态的线程可能没有任何原因就被唤醒,这也是为什么使用wait方法时需要将其放在循环体中的原因。
    1. t在对象m上执行nlock操作。
    2. 如果t是由于调用t.interrupt从等待集合中移除,那么t的中断状态会置为false

规范中关于notifynotifyAll方法的定义如下:

线程t,对象mntm上执行的lock次数。

  • 如果n == 0, 则抛出IllegalMonitorStateException
  • 如果n > 0, 执行notify会将m的等待集合中某个线程移除。
  • 如果n > 0, 执行nofityAll会将m的等待集合中的所有线程移除。

规范中关于中断的定义如下:
线程中断有以下两种方法来执行
Thread.interrupt
ThraedGroup.interrupt
假设线程t执行线程u的中断方法u.interrupt(tu可能是同一个线程),这个调用会使得t的interruption状态变成true.
如果u处于某个对象m的等待集合中,这将会使得uwait方法中恢复,并且抛出InterruptedExcetion
可以通过Thread.isInterrupted方法来判断一个方法是否处于中断状态。Thread.interrupted静态方法可用于一个线程获得它的中断状态并清空中断状态。

JVM字节码

分别查看以下wait方法和notify方法对应的字节码,结果如下:

  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
...

从字节码中可以看出,waitnotify功能由JVM的实现,对应的字节码只是获得相应的监视器锁,并执行相应的native方法。

代码实例分析

  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
  }

根据JVM规范,若线程没有获得obj的监视器锁,即规范中n == 0的情况下,会抛出IllegalMonitorStateException
为什么需要在执行waitnotify方法时先获得对象锁呢?从规范中可以看出,waitnotify操作需要对对象的等待集合进行更改,而这两个更改本身就是竞态条件,因此需要同步。

在JVM的wait方法的实现中,需要释放已经获得对象监视器锁,从而允许执行notify的代码段获得锁并执行。
2. 事件有序
interruptnotify事件必然以一定顺序发生,一个在wait中的线程,同时被另外两个线程notifyinterupt时,线程要么被中断,且通知被另外一个等待的线程获取;要么线程先听到通知恢复执行,未抛出InterruptException,线程的interuptted状态变成true
可以由以下代码验证:

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. 生产者和消费者问题
    并发问题的一个经典场景是生产者和消费者的问题,有多个线程生产,另外多个线程消费。应对这个问题通常会使用阻塞队列。
    假定队列的接口是puttakeput用于生产,take用于消费。非线程安全的queue用于存放元素。使用对象m用的waitnotify方法来实现同步。

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只会随机唤醒一个等级集合中的线程,如果有两个生产者,那么put中的notify唤醒的可能是另一个生产者,从而死锁。因此put方法中应当使用notifyAll。同样原因take方法中也应当使用notifyAll
另外一个需要注意的点是这里新建了一个对象m,并使用m的waitnotify方法来实现同步,既然Java中任何对象都继承了Object对象,那么BlockQueue这个类本身也是有wait和notify方法的,能否直接使用this.waitthis.notify并且在puttake方法上加synchronize呢?
答案是可以的。但是wait方法过程中会解除对象的监视器锁,从而会造成一些对synchronize的语义的干扰。
比如下面的代码

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()方法会导致对象的监视器被解锁,从而导致有两个线程同时进入同一个对象的不同synchronize方法。这会造成不必要的困扰。因此使用waitnotify来实现同步时通常会使用独立的对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值