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的线程还在卡着呢。总之,这里的竞争条件,我们可能在丢失一个通知,如果我们使用缓冲区或 者只有一个产品,生产者线程将永远等待,你的程序也就挂起了。//生产苹果的方法的代码: while(箱子满了){ //使用while而不是if box.wait(); }