wait()、notify()方法原理,以及使用注意事项--丢失唤醒、虚假唤醒

wait、notify原理

在前面以经说到对象锁的本质,重量级锁模式时对象头是一个指向互斥量的指针,实际上互斥量就是一个监视器锁(ObjectMonitor)的数据结构,此时对象的hashCode、分代年龄等信息都会保存到对应的ObjectMonitor中,ObjectMonitor还有一些属性如recursion记录本锁被重入的次数,EntrySet记录想获取本锁的线程集合,WaitSet记录等待本锁的线程,TheOwner记录拥有本锁的线程对象。如下:

(图片来源于网络)

几个线程一起竞争对象的锁(EntrySet),只有一个能成功(acquire),成功的线程记录在The Owner中。调用wait、notify运行流程如下:

       (1) 现有一个对象o,锁正在被线程 t1 持有,调用wait()方法后,线程 t1 将会被"晾到" (实际上仅仅是记录到) Wait Set 结构中。

       (2)然后将会有另一个线程 t2 获取到锁,The Owner记录的变成了 t2 线程。

       (3)t2 线程不需要 o的时,调用o.notify()/o.notifyAll()方法,对象o就会告诉 Wait Set结构中记录的线程们:你们又可以来竞争我啦,我的锁现在没被人持有。

简单的说就是:wait是对象通知持有自己锁的线程释放我的锁,notify()/notifyAll()就是对象通知刚刚被自己晾在一边的线程又可以来竞争我的锁了。我想到了一个比较贴切的比喻:

        客人(线程)来拜访主人(对象),必须获得主人的时间权(锁),且主人同时只能接待一人(互斥)。

        正在客厅接待一名客人时,因为一些原因主人必须先接待另一位客人,这时主人请当前客人去另一间房里等待,让出自己的时间权(wait方法)

        主人在客厅接待另一位客人,接待完毕后,让前一位(也可能有几位)在另一间房等待的客人再来到客厅,继续接待(notify/notifyAll方法)

wait、notify要放在同步块中

其实很简单,如果不在同步块中,调用this.wait()时当前线程都没有取得对象的锁,又谈何让对象通知线程释放锁、或者来竞争锁呢?如果确实不放到同步块中,则会产生 Lost-wake的问题,即丢失唤醒,以上一篇中生产者消费者例子来说:

       1 箱子发现自己满了调用box.wait()通知生产者等待,但是由于wait没在同步块中,还没等生产者接到wait信号进入等待,消费者线程就插队执行消费箱子苹果的方法了(因为wait不在同步块中,也就是调用时箱子的锁没被占有,所以箱子的消费方法是可以被消费者插队调用的)。

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

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

总结就是,由于wait不在同步块中,所以对象执行wait()到线程接到通知进入等待这段时间是可以被其他线程插队,如果这时插队的线程把notify信号发出则会被忽略,因为本来要被wait的线程还在卡着呢。总之,这里的竞争条件,我们可能在丢失一个通知,如果我们使用缓冲区或者只有一个产品,生产者线程将永远等待,你的程序也就挂起了。
 
虚假唤醒
notify/notifyAll时唤醒的线程并不一定是满足真正可以执行的条件了。比如对象o,某线程不满足A条件时发出o.wait(),然后另一线程不满足条件B时也发出o.wait;后来某时间条件B满足了,发出o.notify(),唤醒对象o的等待池里的对象,但是唤醒的线程有可能是因为条件A进入等待的线程,这时把他唤醒条件但是A还是不满足。这是底层系统决定的一个小遗憾。为了避免这种情况,判断调用o.wait()的条件时必须使用while,而不是if,这样在虚假唤醒后会继续判断是否满足A条件,不满足说明是虚假唤醒又会调用o.wait()。
//生产苹果的方法的代码:
while(箱子满了){   //使用while而不是if
    box.wait();      
}
原文地址:https://www.cnblogs.com/shen-qian/p/11265631.html