暮光博客

小心你讨厌的东西,因为你很可能被它塑造成某种形状

多线程中的「lost wake up 问题」

技术 3 条评论

问:Java 多线程中 wait() 方法为什么要放在同步块中?
答:为了避免「lost wake up 问题」,即「无法唤醒问题」。

什么是「lost wake up 问题」

我对「lost wake up 问题」的通俗理解:线程 A 调用 wait() 方法进入阻塞状态,接下来没有其他线程去唤醒线程 A,或者其他线程唤醒时机不对(早于线程 A 的 wait() ),导致线程 A 永远阻塞下去。

有中文资料对这个问题作出过解释:Java中wait()方法为什么要放在同步块中?(lost wake-up 问题),文中举了生产者和消费者的例子,但我觉得此文的结论并未触达核心,需要对文中的例子和结论多做一下补充,理解起来会方便一点。

现在有一个生产者线程和消费者线程:
先定义一个 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+1obj.notify(),也就是发出通知,准备唤醒一个阻塞的线程。然后 CPU 调度到消费者,此时消费者开始执行 obj.wait(),线程进入阻塞。但生产者已经早在消费者阻塞前执行了唤醒动作,也就导致消费者永远无法醒来了。

1644918-20190619224558253-730645351.png

随便加个锁能解决「lost wake up 问题」吗

不能,举个例子。

定义一把锁:

Lock lock1 = new Lock();

生产者伪代码:

lock1.lock();
obj.count++;
obj.notify();
lock1.unlock();

消费者伪代码:

lock1.lock();
while(count<=0)
    obj.wait();
obj.count--;
lock1.unlock();

两个线程启动,obj.count 初始值为 0。假设消费者先竞争到锁,while 中的 obj.count<=0 条件满足,执行 obj.wait() 使线程进入阻塞状态,lock1 锁没有被释放,所以生产者拿不到锁,也就无法 obj.notify() 通知消费者醒来,消费者将永远阻塞下去。

Java 中什么锁才能解决「lost wake up 问题」

只有上述例子中的 obj 对象锁才能避免这个问题,也就是将 obj.wait()obj.notify() 放进 obj 对象锁的同步块中。如果锁的不是例子中的 obj 对象,Java 就会抛出 IllegalMonitorStateException 异常。

生产者伪代码:

synchronized (obj) {
    obj.count++;
    obj.notify();
}

消费者伪代码:

synchronized (obj) {
    while(count<=0)
       obj.wait();
    obj.count--;
}

Java 中对 wait() 方法的注释中提到:线程在调用 obj.wait() 前必须要拿到当前 obj 对象的监视器 monitor 对象,即 obj 的锁。只有这样,当执行到 obj.wait() 时,该线程才可以暂时让出 obj 的同步锁并停止对锁的竞争,让其他正在等待此锁的线程可以得到同步锁并运行。

在上述例子中,消费者执行到 obj.wait() 时,让出了 obj 锁,停止了对锁的竞争,进入阻塞状态,紧接着生产者竞争到 obj 锁,执行了 obj.notify() 方法,唤醒了消费者,使消费者线程从阻塞状态重新回到就绪状态。

这里要注意的是,obj.notify() 并不是让生产者马上释放锁,也不是让消费者马上得到锁,而是通知消费者线程可以重新去参与锁的竞争了。

评论区
选择表情选择表情
  1. java一个高薪的编程程序。

    回复
  2. 评论区样式不错, icon_wink.gif

    回复
  3. 玩的是嘛

    沙发,我就爱看这种 icon_wink.gif

    回复