使用wait方法和notify方法用于线程间通信的正确姿势

背景解释

在 Java 中可以用 wait、notify 和 notifyAll 来实现线程间的通信。。举个例子,如果你的Java程序中有两个线程——即生产者和消费者,那么生产者可以通知消费者,让消费者开始消耗数据,因为队列缓 冲区中有内容待消费(不为空)。相应的,消费者可以通知生产者可以开始生成更多的数据,因为当它消耗掉某些数据后缓冲区不再为满。

我们可以利用wait()来让一个线程在某些条件下暂停运行。例如,在生产者消费者模型中,生产者线程在缓冲区为满的时候,消费者在缓冲区为空的时 候,都应该暂停运行。如果某些线程在等待某些条件触发,那当那些条件为真时,你可以用 notify 和 notifyAll 来通知那些等待中的线程重新开始运行。不同之处在于,notify 仅仅通知一个线程,并且我们不知道哪个线程会收到通知,然而 notifyAll 会通知所有等待中的线程。换言之,如果只有一个线程在等待一个信号灯,notify和notifyAll都会通知到这个线程。但如果多个线程在等待这个信 号灯,那么notify只会通知到其中一个,而其它线程并不会收到任何通知,而notifyAll会唤醒所有等待中的线程。

如何使用Wait

尽管关于wait和notify的概念很基础,它们也都是Object类的函数,但用它们来写代码却并不简单。

  1. 例如在错误的地方使用 synchronized 关键词,表现是不知道synchronized同步快怎么使用,同步那个对象呢。
  2. 没有用正确的对象调用wait,不知道对什么对象调用wait方法
  3. 或者没有遵循规范的代码方法。 关于wait方法是有一个规范的写法的

第一个问题就是,我们怎么在代码里使用wait()呢? 正确的方法是对在多线程间共享的那个Object来调用wait。在生产者消费者问题中,这 个共享的Object就是那个缓冲区队列。
第二个问题是,既然我们应该在synchronized的函数或是对象里调用wait,那哪个对象应该被synchronized呢?答案是,那个 你希望上锁的对象就应该被synchronized,即那个在多个线程间被共享的对象。在生产者消费者问题中,应该被synchronized的就是那个 缓冲区队列。(我觉得这里是英文原文有问题……本来那个句末就不应该是问号不然不太通……)

现在你知道wait应该永远在被synchronized的同步块下和那个被多线程共享的对象上调用,下一个一定要记住的问题就是,你应该永远在 while循环,而不是if语句中调用wait。
因为线程是在某些条件下等待的——在我们的例子里,即“如果缓冲区队列是满的话,那么生产者线程应该等 待”,你可能直觉就会写一个if语句。但if语句存在一些微妙的小问题,导致即使条件没被满足,你的线程你也有可能被错误地唤醒。所以如果你不在线程被唤 醒后再次使用while循环检查唤醒条件是否被满足,你的程序就有可能会出错——例如在缓冲区为满的时候生产者继续生成数据,或者缓冲区为空的时候消费者 开始消耗数据。所以记住,永远在while循环而不是if块中使用wait!

基于以上认知,下面这个是使用wait和notify函数的规范代码模板:

  1. // The standard idiom for calling the wait method in Java
  2. synchronized (sharedObject) {
  3. while (condition) {
  4. sharedObject.wait();
  5. // (Releases lock, and reacquires on wakeup)
  6. }
  7. // do action based upon condition e.g. take or put into queue
  8. }

在while循环里使用wait的目的,是在线程被唤醒的前后都持续检查条件是否被满足。生产者线程如果条件并未改变,那么这个线程并不能被唤醒,可以防止对缓冲区队列误操作。

这是用wait/notifyAll方法对一个列表操作的生产者和消费者模型。
这个方法是对列表进行设置的方法。用一个生产者线程调用这个方法。

public void setValue() {
        try {
            synchronized (lock) {
                while (!ValueObject.value.equals("")) {
                    // 数据集不为空,那么生产者要休息
                    System.out.println("生产者 " + Thread.currentThread().getName() + " WAITING ***");
                    lock.wait();
                }
                System.out.println("生产者 " + Thread.currentThread().getName() + "RUNNABLE ");
                String value = System.currentTimeMillis() + "_" + System.nanoTime();
                ValueObject.value = value;
                lock.notifyAll();

            }
        } catch (Exception e) {
            // TODO: handle exception
        }
    }

//这是一个获取列表数据的方法,由消费者线程调用。

public void getValue() {
        try {
            synchronized (lock) {
                while (ValueObject.value.equals("")) {
                    System.out.println("消费者" + Thread.currentThread().getName() + " WAITING**");
                    lock.wait();
                }
                System.out.println("消费者" + Thread.currentThread().getName() + " Runnable");
                ValueObject.value = "";
                lock.notifyAll();
            }
        } catch (Exception e) {
            // TODO: handle exception
        }

    }

本文重点:
1. 你可以使用wait和notify函数来实现线程间通信。你可以用它们来实现多线程(>3)之间的通信。
2. 永远在synchronized的函数或对象里使用wait、notify和notifyAll,不然Java虚拟机会生成 IllegalMonitorStateException。
3. 永远在while循环里而不是if语句下使用wait。这样,会在线程暂停恢复后都检查wait的条件,并在条件实际上并未改变的情况下处理唤醒通知。
4. 永远在多线程间共享的对象(在生产者消费者模型里即缓冲区队列)上使用wait。
5. 基于前文提及的理由,更倾向用 notifyAll(),而不是 notify()。用notify()方法只是讲一个因为wait操作处于等待的线程唤醒。可能生产者只是唤醒生产者。而没有唤醒消费者。所以为了防止多线程下假死情况都是用notifyAll()方法

你可能感兴趣的:(java-8学习记录)