引言

java中的Object类提供了wait、notify方法,这篇文章我们来看这两类方法的使用以及我们能通过这些方法做什么。

方法列表

这些方法的声明如下。

  1. public final native void notify();
  2. public final native void notifyAll();
  3. public final native void wait(long timeout) throws InterruptedException;
  4. public final void wait(long timeout, int nanos) throws InterruptedException {};
  5. public final void wait() throws InterruptedException {}

首先,这些方法都是在Object类上定义的并且都是final的,也就是说,任何引用类型的对象,都会有这些方法,并且不能重写。

wait()方法

wait()方法会导致当前线程释放线程持有的该对象的锁,然后处于wait状态,直到另外一个线程执行了这个对象的notify()方法或者notifyAll()方法,或者达到指定的时间限制(timeout参数)。

执行wait()方法必须首先持有锁

看下面的例子:

  1. public class WaitNotify {
  2. static final Object lock = new Object();
  3. public static void main(String[] args) throws InterruptedException {
  4. lock.wait();
  5. }
  6. }

我直接在main方法里面调用了lock这个对象的wait()方法。这样会报错:
waitException.png
java.lang.IllegalMonitorStateException这个异常就是专门提示这种错误的:当一个线程尝试执行wait()类方法(上面表格中的三个wait方法)或者notify()、notifyAll()方法时没有持有该对象的锁,就会抛出这个异常,下面是JDK文档中的描述:

  1. /**
  2. * Thrown to indicate that a thread has attempted to wait on an
  3. * object's monitor or to notify other threads waiting on an object's
  4. * monitor without owning the specified monitor.
  5. *
  6. * @author unascribed
  7. * @see java.lang.Object#notify()
  8. * @see java.lang.Object#notifyAll()
  9. * @see java.lang.Object#wait()
  10. * @see java.lang.Object#wait(long)
  11. * @see java.lang.Object#wait(long, int)
  12. * @since JDK1.0
  13. */

那怎么获得锁呢?肯定就是通过synchronized关键字了,我们有三种方式可以获得该对象的锁:

  • 通过执行这个对象的同步实例方法。
  • 通过执行同步块,该同步块是通过该对象锁定的。
  • 对于Class类的对象,通过执行这个对象的静态方法。

所以我们把上面的代码改一下:

  1. public class WaitNotify {
  2. static final Object lock = new Object();
  3. public static void main(String[] args) throws InterruptedException {
  4. synchronized (lock){
  5. lock.wait();
  6. }
  7. }
  8. }

再执行就没有问题了。notify方法在执行时也要先获得锁,也就是说,wait和notify的使用,还是离不开synchronized。

wait()方法执行之后线程的状态

继续用前面的示例,我们来看执行wait方法后,线程的状态。
首先通过jps找到当前的java进程,然后通过jstack查看线程状态:
statusFinding.png
main方法的状态如下:
waitStatus.png
是waiting状态,正在等待object monitor,处于这个状态的线程不能被调度。注意,虽然是在等待对象上的锁,但是不是blocked状态。

wait()方法会立即释放锁

当线程调用wait方法后,线程在wait()方法处处于等待状态不会继续执行,此时它持有的锁资源已经被立即释放,其他请求该对象锁的线程已经可以通过锁竞争获取到锁,看下面的例子:

  1. public class WaitNotify {
  2. static final Object lock = new Object();
  3. public static void main(String[] args) {
  4. new Thread(new Wait(),"wait_thread").start();
  5. new Thread(new Notify(),"notify_thread").start();
  6. }
  7. static class Wait implements Runnable{
  8. @Override
  9. public void run() {
  10. synchronized (lock){
  11. System.out.println("wait线程获得了锁,马上执行wait方法");
  12. try {
  13. lock.wait();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. }
  19. }
  20. static class Notify implements Runnable{
  21. @Override
  22. public void run() {
  23. synchronized (lock){
  24. System.out.println("notify线程获得了锁");
  25. }
  26. }
  27. }
  28. }

第一个线程首先获取lock对象的锁进入同步块,然后执行wait方法释放该锁并处于waiting状态,第二个线程马上就会拿到锁进入同步块。wait方法这样做是有原因的,因为执行完wait方法后线程已经不能继续执行了,如果不释放锁,就会造成其他线程也无法拿到锁。
输出如下:

  1. wait线程获得了锁,马上执行wait方法
  2. notify线程获得了锁

稍后我们会看到,当执行notify方法时,当前线程并不会立即释放锁,而是等同步块或者同步方法执行完再释放。

wait()的唤醒

以下四种事件的发生会唤醒执行了wait()方法的线程:

  • 一些其他的线程调用了对象的notify()方法并且线程T正好被选中唤醒。
  • 一些其他的线程执行了对象的notifyAll()方法。
  • 一些其他的线程对线程T执行了Thread.interrupt()方法。
  • 达到wait()方法中指定的时间,如果timeout参数是0,那么时间不会作为被唤醒的考虑条件,也就是只能用上面三种事件来唤醒。

    这四个事件发生之后,线程T会从对象的等待集中移除并且能够被线程调度,然后跟其他线程竞争该对象的同步权。
    这里有一点需要注意,就是wait()方法执行完后该线程的状态是waiting,如果上面四个事件发生,这个线程就能重新竞争锁,那么它的状态会变成什么呢?看下面的例子:

    1. public class WaitNotify {
    2. static final Object lock = new Object();
    3. public static void main(String[] args) {
    4. new Thread(new Wait(),"wait_thread").start();
    5. new Thread(new Notify(),"notify_thread").start();
    6. }
    7. static class Wait implements Runnable{
    8. @Override
    9. public void run() {
    10. synchronized (lock){
    11. System.out.println("wait线程获得了锁,马上执行wait方法");
    12. try {
    13. lock.wait();
    14. System.out.println("wait方法执行完成");
    15. } catch (InterruptedException e) {
    16. e.printStackTrace();
    17. }
    18. }
    19. }
    20. }
    21. static class Notify implements Runnable{
    22. @Override
    23. public void run() {
    24. synchronized (lock){
    25. System.out.println("notify线程获得了锁,马上执行notifyAll方法");
    26. lock.notifyAll();
    27. try {
    28. Thread.sleep(300000);
    29. } catch (InterruptedException e) {
    30. e.printStackTrace();
    31. }
    32. }
    33. }
    34. }
    35. }

    在notify_thread线程执行了notifyAll方法之后,我们查看wait线程的状态:
    image.png
    是BLOCKED状态,为什么不是WAITING状态呢?因为notifyAll()方法执行之后,wait线程又可以重新竞争锁了(此时notify线程并没有将锁释放),在竞争锁的过程中,状态就是BLOCKED。
    notify线程的状态如下:
    image.png
    它由于调用了sleep方法而处于TIMED_WAITING状态。
    注意,notify线程并不会在调用了notify()或者notifyAll()方法后立即释放锁,而是在执行完同步块或者同步方法之后再释放锁。

    notify()、notifyAll()方法

    notify()方法唤醒一个在等待对象锁的线程。如果有线程在这个对象上面等待,它们中的一个会被选择来唤醒。
    notifyAll() 方法会唤醒所有在等待对象锁的线程,但是它们中只有一个会再次获得锁从wait方法返回。
    执行notify()、notifyAll()方法的线程要首先获取锁,这一点与wait方法一样。
    在释放锁这一点上,notify、notifyAll()方法与wait不同,线程在调用wait方法后会立即释放锁资源,而线程调用notify、notifyAll方法后并不会立刻释放锁,而是在执行完同步块后再释放。这个我们在上面的示例中已经看到了,这里不再赘述。
    对wait和notify的细节做一下总结,就是下面几点:

  1. 使用wait()、notify()和notifyAll()时需要先对调用对象加锁(synchronized关键字)。
  2. 调用wait()方法后,线程会立即释放锁,状态由RUNNING变为WAITING,并将当前线程放置到对象的等待队列。
  3. notify()或notifyAll()方法调用后,等待线程依旧不会从wait()方法返回(没有获得锁),需要调用notify()或者notifyAll()的线程释放锁(执行完同步块或者同步方法)之后,等待线程才能从wait()返回。

    生产者、消费者模型实现

    基于wait、notify的等待/通知机制,我们可以实现生产者、消费者模型。生产者不断进行数据生产、消费者不断进行数据消费,当没有数据时,消费者线程等待,当数据达到限制时,生产者线程等待,下面是一个完整的示例: ```java public class WaitNotify { private static int count = 0; static final Object lock = new Object();

    public static void main(String[] args) {

    1. ExecutorService producers = new ThreadPoolExecutor(5,5,1, TimeUnit.SECONDS,new LinkedBlockingDeque<>());
    2. ExecutorService consumers = new ThreadPoolExecutor(3,3,1,TimeUnit.SECONDS,new LinkedBlockingDeque<>());
    3. for(int i=0;i<1000;i++){
    4. producers.submit(new Producer());
    5. consumers.submit(new Consumer());
    6. }

    }

    static class Consumer implements Runnable {

    1. @Override
    2. public void run() {
    3. try {
    4. Thread.sleep(1000);
    5. } catch (InterruptedException e) {
    6. e.printStackTrace();
    7. }
    8. synchronized (lock) {
    9. while (count == 0) {
    10. try {
    11. Thread.sleep(1000);
    12. } catch (InterruptedException e) {
    13. e.printStackTrace();
    14. }
    15. System.out.println(Thread.currentThread().getName() + "获取时数量为0,wait......");
    16. try {
    17. lock.wait();
    18. } catch (InterruptedException e) {
    19. e.printStackTrace();
    20. }
    21. }
    22. count--;
    23. System.out.println("消费者" + Thread.currentThread().getName() + "消费后count=" + count);
    24. lock.notifyAll();
    25. }
    26. }

    }

  1. static class Producer implements Runnable {
  2. @Override
  3. public void run() {
  4. try {
  5. Thread.sleep(1000);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. synchronized (lock) {
  10. while (count == 100) {
  11. try {
  12. lock.wait();
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. count++;
  18. System.out.println("生产者" + Thread.currentThread().getName() + "生产后count=" + count);
  19. lock.notifyAll();
  20. }
  21. }
  22. }

}

``` 这个例子中,用int类型数据count的增加和减少来模拟数据的生产和消费,count最大是100,当达到100时,生产者线程等待,count最小是0,当count是0时,消费者线程等待。生产者和消费者线程是线程池产生的。生产者和消费者分别执行1000次生产和消费操作,最终都执行完成后,count的数量应该是0。这里就不再展示执行结果了。
当然我们也能在Pruducer和Consumer的Run方法最外层加上while(true),这样每个生产者线程就能一直不停地进行判断和生产操作,每个消费者线程能一直不停地进行判断和消费操作,这样的话,我们也就不需要用线程池来不断地添加任务了,生产者和消费者线程都只需要一个。

小结

wait和notify方法都需要synchronized关键字来获取锁,然后在释放锁时进行线程间通信来实现等待/通知机制。这两种方法调用后锁的释放时机不同,wait方法执行后即可释放锁,notify方法执行后需要等线程执行完同步块或同步方法才能释放锁,并且这两种方法调用后线程的状态也不同,我们需要重点理解这些问题。
通过等待/通知机制,我们可以实现生产者、消费者模型,这并不是生产者、消费者模型的唯一实现方式,后面我们会看到其他的实现方式。