为何wait和notify方法必须加锁?

为何wait和notify方法必须加锁?

是为了避免Lost Wake Up问题,即丢失唤醒问题。

所谓Lost Wake Up,指的是:线程A调用wait()方法进入阻塞状态,接下来没有其他线程去唤醒线程A,或者其他线程唤醒的时机不对(早于线程A的wait()),导致线程A永远阻塞下去。

下面以一个生产者和消费者的案例来说明上述问题。

假如我们有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务可以简化成将count加一,而后唤醒消费者;消费者则是将count减一,而后在减到0的时候陷入睡眠,代码如下:

先定义一个 obj 对象,并将其 count 属性的初始值设置为 0:

Object obj = new Object();
obj.count = 0;
  • 生产者
obj.count++;
obj.notify();
  • 消费者
while(obj.count<=0)
    obj.wait();
obj.count--;

很明显,上述代码会出现问题,因为生产者和消费者的操作均不是原子的。两个线程启动,消费者检查 obj.count 的值,发现 obj.count <= 0 条件成立,但这时由于 CPU 的调度,发生上下文切换,生产者开始工作,执行了 count+1 和 obj.notify(),也就是发出通知,准备唤醒一个阻塞的线程。然后 CPU 调度到消费者,此时消费者开始执行 obj.wait(),线程进入阻塞。但生产者已经早在消费者阻塞前执行了唤醒动作,也就导致消费者永远无法醒来了。

《为何wait和notify方法必须加锁?》

解决方法很简单咯,把上述操作变为原子操作即可:

  • 优化后的生产者
synchronized (obj) {
    obj.count++;
    obj.notify();
}
  • 优化后的消费者
synchronized (obj) {
    while(count<=0)
       obj.wait();
    obj.count--;
}

接下来,还有个问题,如果消费者在执行obj.wait()之前宕掉了,岂不是锁就永远不会释放了,答案是不会的。

synchronized同步块执行代码过程中,一旦出现代码错误或线程宕掉,会自动释放掉持有的锁,避免了死锁的问题。

Condition的await()和signal()方法必须加锁同理,与synchronsized关键字不同的是,Lock必须由用户手动执行加锁/释放锁操作,当持有锁的线程发生异常时,该线程不会自动释放锁,可能会导致死锁,故Lock必须在try-catch块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

这也解释了既然调用condition.await()方法后已经释放了锁,为何还得在finally块中再次手动得释放锁,其只是为了防止该线程还没来得及执行await()方法就挂掉进而造成的死锁问题。

参考

https://www.cnblogs.com/sunweiye/p/11055550.html

    原文作者:WenWu_Both
    原文地址: https://blog.csdn.net/WenWu_Both/article/details/106520799
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞