关于wait/notify

Posted zjoe80

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于wait/notify相关的知识,希望对你有一定的参考价值。

一.wait/notity的使用

wait()方法可以使线程进入等待状态,而notify()可以使等待的状态唤醒。

这样的同步机制十分适合生产者、消费者模式:消费者消费某个资源,而生产者生产该资源。

当该资源缺失时,消费者调用wait()方法进行自我阻塞,等待生产者的生产;生产者生产完毕后调用notify/notifyAll()唤醒消费者进行消费。

例子1

代码示例:

public class ThreadTest {

    static final Object obj = new Object();

    private static boolean flag = false; //flag标志表示资源的有无。

    public static void main(String[] args) throws Exception {

        Thread consume = new Thread(new Consume(), "Consume");
        Thread produce = new Thread(new Produce(), "Produce");
        consume.start();
        Thread.sleep(1000);
        produce.start();

        try {
            produce.join();
            consume.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 生产者线程
    static class Produce implements Runnable {

        @Override
        public void run() {

            synchronized (obj) {
                System.out.println("进入生产者线程");
                System.out.println("生产");
                try {
                    TimeUnit.MILLISECONDS.sleep(2000);  //模拟生产过程
                    flag = true;
                    obj.notify();  //通知消费者
                    TimeUnit.MILLISECONDS.sleep(1000);  //模拟其他耗时操作
                    System.out.println("退出生产者线程");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    //消费者线程
    static class Consume implements Runnable {

        @Override
        public void run() {
            synchronized (obj) {
                System.out.println("进入消费者线程");
                System.out.println("wait flag 1:" + flag);
                while (!flag) {  //判断条件是否满足,若不满足则等待
                    try {
                        System.out.println("还没生产,进入等待");
                        obj.wait();
                        System.out.println("结束等待");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("wait flag 2:" + flag);
                System.out.println("消费");
                System.out.println("退出消费者线程");
            }
        }
    }
}

运行结果为:

进入消费者线程
wait flag 1:false
还没生产,进入等待
进入生产者线程
生产
退出生产者线程
结束等待
wait flag 2:true
消费
退出消费者线程

理解了输出结果的顺序,也就明白了wait/notify的基本用法。有以下几点需要知道:

在示例中没有体现但很重要的是,wait/notify方法的调用必须处在该对象的锁(Monitor)中,在调用这些方法时首先需要获得该对象的锁,否则会爬出IllegalMonitorStateException异常。

从输出结果来看,在生产者调用notify()后,消费者并没有立即被唤醒,而是等到生产者退出同步块后才唤醒执行。

这点其实也好理解,synchronized同步方法(块)同一时刻只允许一个线程在里面,生产者不退出,消费者也进不去。

注意:消费者被唤醒后是从wait()方法(被阻塞的地方)后面执行,而不是重新从同步块开头。

例子2:

箱子中的苹果代表资源,现在有消费者从箱子中拿走苹果,生产者往箱子中放苹果。代码如下:

资源--箱子中的苹果:

public class Box {
int size; int num; public Box(int size, int num) { this.size = size; this.num = num; } public synchronized void put() { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } while (num == 10) { //用while循环检查更好,在下面的wait()结束后还再判断一次,防止虚假唤醒 try { System.out.println("箱子满了,生产者暂停。。。"); this.wait(); //等待消费者消费一个才能继续生产,所以要让出锁 } catch (InterruptedException e) { e.printStackTrace(); } finally { } } num++; System.out.println("箱子有空闲,开始生产。。。"+num); this.notify(); //唤醒可能因为没苹果而等待的消费者 } public synchronized void take() { try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } while (num == 0) { //用while循环检查更好,在wait()结束后还再判断一次,防止虚假唤醒 try { System.out.println("箱子空了,消费者暂停。。。"); this.wait(); //等待生产者生产一个才能继续消费,所以要让出锁 } catch (InterruptedException e) { e.printStackTrace(); } finally { } } num--; System.out.println("箱子有了,开始消费。。。"+num); this.notify(); //唤醒可能因为苹果满了而等待的生产者 } }

生产者、消费者:

public class Consumer implements Runnable {
 
    private Box box;
 
    public Consumer(Box box) {
        this.box= box;
    }
 
    @Override
    public void run() {
        while (true){
            box.take();
        }
 
    }
}
public class Producer implements Runnable {
 
    private Box box;
 
    public Producer(Box box) {
        this.box= box;
    }
 
    @Override
    public void run() {
        while (true){
            box.put();
        }
 
    }
}
public class ConsumerAndProducer {
 
    public static void main(String[] args) {
Box box
= new Box();
Producer p1
= new Producer(box); //生产线程 Consumer c1 = new Consumer(box); //消费线程 new Thread(p1).start(); new Thread(c1).start(); } }

以上,就是生产者消费者模式的Java代码实现。当然,我们完全可以使用JUC包的Lock接口下的类代替Synchronized完成代码同步:

Lock l = new ReentrantLock();
Condition condition = l.newCondition();

l.lock()  //加锁

l.unlock()  //释放锁

condition.await()  //代替wait()

condition.signal()   //代替notify()

除了上述方法,也可以使用JUC包下BlockingQueue接口的阻塞队列完成,那样更简单。

实际上,阻塞队列也是基于上述的基本思想实现的----队列满了就停止装入线程、空了就让取队列元素的线程等待。

上述的Box就是一个阻塞队列的抽象模型(当然阻塞队列比这个还是要复杂很多)。

1、wait、notify要放在同步块中

其实很简单,如果不在同步块中,调用this.wait()时当前线程都没有取得对象的锁,又谈何让对象通知线程释放锁、或者来竞争锁呢?

如果确实不放到同步块中,则会产生 Lost-wake的问题,即丢失唤醒,以生产者消费者例子来说:

(1)箱子发现自己满了调用box.wait()通知生产者等待,但是由于wait没在同步块中,还没等生产者接到wait信号进入等待,

消费者线程就插队执行消费箱子苹果的方法了(因为wait不在同步块中,也就是调用时箱子的锁没被占有,所以箱子的消费方法是可以被消费者插队调用的)。

(2)这时消费者线程从缓冲区消费一个产品后箱子调用box.notify()方法,但生产者此时还没进入等待,因此notify消息将被生产者忽略。

(3)生产者线程恢复执行接收到迟来的wait()信号后进入等待状态,但是得不到notify通知了,一直等待下去。

总结就是,由于wait不在同步块中,所以对象执行wait()到线程接到通知进入等待这段时间是可以被其他线程插队,

如果这时插队的线程把notify信号发出则会被忽略,因为本来要被wait的线程还在卡着呢。

总之,这里的竞争条件,我们可能在丢失一个通知,如果我们使用缓冲区或者只有一个产品,生产者线程将永远等待,你的程序也就挂起了。

2、虚假唤醒

notify/notifyAll时唤醒的线程并不一定是满足真正可以执行的条件了。比如对象o,不满足A条件时发出o.wait(),

然后不满足条件B时也发出o.wait;然后条件B满足了,发出o.notify(),唤醒对象o的等待池里的对象,但是唤醒的线程有可能是因为条件A进入等待的线程,这时把他唤醒条件A还是不满足。

这是底层系统决定的一个小遗憾。为了避免这种情况,判断调用o.wait()的条件时必须使用while,而不是if,这样在虚假唤醒后会继续判断是否满足A条件,不满足说明是虚假唤醒又会调用o.wait()。

 

以上是关于关于wait/notify的主要内容,如果未能解决你的问题,请参考以下文章

关于wait/notify

关于wait/notify

关于wait/notify

为什么wait()和notify()属于Object类

Java多线程_wait/notify/notifyAll方法

多线程的wait/notify的使用