三个问题:
- 为什么
wait
方法必须在synchronized
同步代码块中使用?- 为什么
wait / notify / nojtifyAll
被定义在 Object 类中,而sleep
定义在 Thread 类中?wait / notify
和sleep
方法的异同?
为什么 wait 方法必须在 synchronized 同步代码块中使用?
Object#wait
方法注释:
*The current thread must own this object's monitor lock... |
在使用 wait 方法时,必须把 wait 方法写在 synchronized 保护的循环代码块中,并且始终判断执行条件是否满足,如果满足就往下继续执行,如果不满足就执行 wait 方法,而在执行 wait 方法之前,必须要先持有对象的 monitor 锁,也就是 synchronized 锁。
为什么要这么设计呢?
现在我们假设,如果不要求 wait 方法放在 synchronized 同步代码块中,而是可以随意调用,那么考虑下面这种场景:
Class Demo { |
上述代码中,give
方法负责向 buf
中添加数据,添加完之后执行 notify
方法来唤醒之前等待的线程,而 take
方法负责检查整个
buf
是否为空,如果为空,就调用 wait
方法进入等待,如果不为空就取出一个数据,这也就是生产者消费者思想。
但是这段代码没有在 synchronized 同步代码块中,在某些情况下可能因为处理器的调度而出现一些问题:
- 首先,消费者线程调用
take
方法并判断buf.isEmpty()
,若为 true 代表 buf 为空,消费者线程希望进入等待,但是在执行wait()
方法之前,处理器调度运行了其他线程,当前线程被暂停了,所以此时还没来得及执行wait()
方法; - 然后,生产者线程开始执行,执行完整个
give
方法过程,它向 buf 中添加了数据,并且执行了notify
方法,但是notify
并没有任何效果,因为消费者线程的wait
方法还没有执行,所以没有在等待中的线程被唤醒; - 此时,刚在被调度器暂停的消费者线程继续执行
wait
方法并进入了等待。
可以看出,虽然消费者判断了 buf 是否为空,但是在真正执行
wait()
方法时,之前判断的结果已经「过期」了,不再符合最新的场景,因为此时「判断-执行」不再是一个原子操作,它在中间被打断,是线程不安全的。
更极端一点,如果此时没有更多的生产者进行生产,消费者可能陷入无休止的等待中。
正是因为 wait()
方法所在的 take()
方法没有被 synchronized 保护,所以 while 判断和 wait
方法无法构成一个原子操作,那么此时整个程序就很容易出现错误的结果。
「使用 synchronized 之后」
Class Demo { |
这样就可以确保 notify()
永远不会在
buf.isEmpty()
和 wait()
方法之间被调用,提升了程序的安全性。
除此之外,wait()
方法也会释放 monitor
锁,这也要求线程需要进入到 synchronized 同步代码块内部持有这把锁。
PS:此处可能还存在一个「虚假唤醒」的问题,线程可能在既没有被
notify / notifyAll
、也没有被中断或者超时的情况下被唤醒。虽然在实际生产中,虚假唤醒发生的概率很小,但是程序需要保证在发生虚假唤醒时的正确性,所以就需要采用循环的结构:
while(condition does not hold)
obj.wait();这样即便被虚假唤醒了,也会再次检查循环的条件,如果不满足,就会继续
wait()
,也就消除了虚假唤醒的风险了。
为什么 wait / notify / nojtifyAll 被定义在 Object 类中,而 sleep 定义在 Thread 类中?
这个问题主要有几点原因:
- 因为 Java 中每个对象都有一把 monitor
监视器锁,每个对象都可以上锁,于是在对象头中有一块专门用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,
wait / notify / notifyAll
都是对象级别的操作,锁属于对象,所以将这些方法定义在 Object 类中是合适的,因为 Object 是所有对象的父类; - 如果把
wait / notify / notifyAll
定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时wait()
方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然是要让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程; - 因为
sleep()
是让当前线程暂停执行,不涉及到对象类,也不需要获得对象锁。
wait / notify 和 sleep 方法的异同?
主要是对比 wait 和 sleep 方法。
1)相同点:
- 都可以让线程阻塞;
- 都可以响应中断,即在阻塞过程中如果收到了中断信号,都可以进行响应,并抛出
InterruptedException
异常。
2)不同点:
wait()
方法必须在 synchronized 同步代码块中执行,sleep()
方法没有这个要求;- 在同步代码块中执行
wait()
方法时,线程会主动释放 monitor 锁,而执行sleep()
函数不会释放 monitor 锁; sleep()
方法要求必须定义一个超时时间,时间到期后会主动恢复,而对于没有参数的wait()
方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复;wait / notify
是 Object 类的方法,而sleep
是 Thread 类的方法。