关于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的主要内容,如果未能解决你的问题,请参考以下文章