文档章节

关于Object#wait() 的虚假唤醒

暗中观察
 暗中观察
发布于 02/26 08:35
字数 1186
阅读 8
收藏 0

先看关于wait()方法的javadoc描述

Causes the current thread to wait until another thread invokes the notify() method or the 
notifyAll() method for this object. In other words, this method behaves exactly as if 
it simply performs the call wait(0).
The current thread must own this object's monitor. 
The thread releases ownership of this monitor and waits until
 another thread notifies threads waiting on this object's monitor
 to wake up either through a call to the notify method or the
 notifyAll method. The thread then waits until it can re-obtain
 ownership of the monitor and resumes execution.
As in the one argument version, interrupts and spurious wakeups 
are possible, and this method should always be used in a loop:

译文:
线程会等待,直到另一个线程执行这个对象的notify或notifyAll 方法,换句话说就是此方法相当于执行了wait(0)方法,
此方法执行是当前线程必须拥有此对象的监视器(否则会报IllegalMonitorStateException 异常),
当前线程会释放 此对象监视的所有权,
并等待直到另一个线程通过调用notify或notifyAll方法通知此等待对象监视器的线程唤醒,然后线程等待,
直到它重新获取到监视器所有权并继续执行。和单参数版本一样,
中断和虚假唤醒是可能的,并且该方法应始终在循环中使用。

而最后一句话便是本篇文章的重点:虚假唤醒。先看例子:

消费方法

 public synchronized void customer(){//消费
        if(product <= 0){
            System.out.println(Thread.currentThread().getName()+",产品缺货:");
            try {
                System.out.println("cus1111");
                this.wait();
                System.out.println("cus2222");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.println(Thread.currentThread().getName()+",消费了一个产品:"+ product--);
            this.notifyAll();

        }
    }

生成方法

 public synchronized void product(){//生产
        if(product >= 1){
            System.out.println(Thread.currentThread().getName()+",产品满了:");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }else{
            System.out.println(Thread.currentThread().getName()+",生产了一个产品:"+ ++product);
            this.notifyAll();

        }
    }

消费次数和生成次数是一样的前提下,存在这样一种情况:

生成者还有两次执行权,消费者也有两次。假设此时产品数为0,生产者抢到了cpu执行权,进行了一次生产,然后生成者有抢到了cpu执行权,执行了一次,并等待在那了,之后生产者可以执行的次数为0了。

接着消费者得到了cpu执行权,进行了一次消费,并唤醒了等待中的生产者。之后假设消费者抢到了cpu执行权,就先进行消费,此时produce为0,消费者等待在那了,此时生产者得到了cpu执行权,从wait方法往下执行,不会执行notifyAll方法,等待中的消费者无法被唤醒,然后生产者线程就退出了,无人再来唤醒消费者线程了。

结果如下,程序无法正常退出

解决办法:将else去掉。这样就能在生产者被唤醒后,执行notifyAll方法,从而唤醒等待中的消费者。但这样是存在问题的,什么问题呢?那就是本篇文章的主角:虚假唤醒。

我们先在生产者线程谁200毫秒,模拟业务操作,并开启两个生产者,两个消费者,同时对Clerk 的产品 进行操作,主要代码如下;

  Clerk clerk=new Clerk();
        new Thread(new Prod(clerk), "prod-1").start();
        new Thread(new Customer(clerk),"cus-1").start();
        new Thread(new Prod(clerk), "prod-2").start();
        new Thread(new Customer(clerk),"cus-2").start();



---------------------------------------------------------------

  @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.prod();
        }
    }

结果:程序能正常退出,但却产生了负数产品

原因:一旦两个消费者同时进入了wait方法,同时释放了cpu执行权,然后由生产者进行notifyAll 进行唤醒,两个消费者线程同时对produce 进行消费,所以才产生了负数产品。

解决:根据wait的javadoc建议,将wait() 放在循环里进行操作即可,这样当两个消费者同时被唤醒时,就会再判定一次,从而不会产生虚假唤醒问题。

最后附上完整的代码

public class WatiTest {

    public static void main(String[] args) {
        Clerk clerk=new Clerk();
        new Thread(new Prod(clerk), "prod-1").start();
        new Thread(new Customer(clerk),"cus-1").start();
        new Thread(new Prod(clerk), "prod-2").start();
        new Thread(new Customer(clerk),"cus-2").start();
    }

}

class Clerk{
    int produce=0;
    public synchronized void prod(){
        if(produce >= 1){
            System.out.println(Thread.currentThread().getName()+",满了");
            try {
                this.wait();
                System.out.println(Thread.currentThread().getName()+",被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName()+",生产了一个产品:"+ ++produce);
            this.notifyAll();
    }
    public synchronized void sale(){
        if(produce <= 0){
            System.out.println(Thread.currentThread().getName()+",缺货");
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
            System.out.println(Thread.currentThread().getName()+",消费了一个产品:"+ produce--);
            this.notifyAll();
    }
}
class Prod implements Runnable{

    Clerk clerk;

    public Prod(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            clerk.prod();
        }
    }
}
class Customer implements Runnable{

    Clerk clerk;

    public Customer(Clerk clerk) {
        this.clerk = clerk;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            clerk.sale();
        }
    }
}

 

© 著作权归作者所有

暗中观察

暗中观察

粉丝 7
博文 113
码字总数 42959
作品 0
惠州
私信 提问
Java多线程8 条件对象Condition

Java多线程目录 1 简介 Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法...

香沙小熊
2018/12/03
0
0
关于wait()和notify()

1.wait()和notify()的涉及初衷是为了解决线程之间通讯的问题,他们都是java.lang.Object的三个方法。Java通过内建的等待机制来允许线程在等待信号的时候变为非运行状态。 2.为了调用wait()或...

夕水溪下
2013/06/04
0
0
Java 线程状态之 TIMED_WAITING

在上一篇章中我们谈论了 WAITING 状态,在这一篇章里,我们来看剩余的最后的一个状态:TIMEDWAITING(限时等待)。 定义 一个正在限时等待另一个线程执行一个动作的线程处于这一状态。 A th...

国栋
2016/12/14
293
2
java5条件阻塞Condition的应用

一、概述 1、在等待 Condition 时,允许发生“虚假唤醒”,这通常作为对基础平台语义的让步。对于大多数应用程序,这带来的实际影响很小,因为 Condition 应该总是在一个循环中被等待,并测试...

哎小艾
2018/01/18
0
0
C++11 线程、锁和条件变量

std::thread类代表了一个可执行的线程,它来自头文件。与其它创建线程的API(比如 Windows API中的CreateThread)不同的是, 它可以使用普通函数、lambda函数以及仿函数(实现了operator()...

oschina
2013/05/27
15.4K
16

没有更多内容

加载失败,请刷新页面

加载更多

Dubbo服务暴露与注册

前面的文章中,我们讲解了Dubbo是如何进行配置的属性的初始化的,并且讲到,Dubbo最终会将所有的属性参数都封装为一个URL对象,从而以这个URL对象为基准传递参数。本文则主要讲解Dubbo是如何...

爱宝贝丶
26分钟前
0
0
Leetcode PHP题解--D88 696. Count Binary Substrings

D88 696. Count Binary Substrings 题目链接 696. Count Binary Substrings 题目分析 给定一个01字符串,返回仅用连续的0和1串所能组成的二进制字符串个数。 例如,00110011,就包含0011,0...

skys215
今天
2
0
基础工具类

package com.atguigu.util;import java.sql.Connection;import java.sql.SQLException;import java.util.Properties;import javax.sql.DataSource;import com.alibaba.druid......

architect刘源源
今天
56
0
P30 Pro劲敌!DxO官宣新机:排行榜又要变

5月26日晚间,DxOMark官方推特预告,将在5月27日公布一款新机型的DxOMark评分,猜猜是哪款? 网友猜想的机型有:红米K20、谷歌Pixel 3a、索尼Xperia 1、诺基亚9 PureView等。 DxOMark即将公布...

linux-tao
昨天
18
0
Ubuntu18.04.2窗口过小不能自适应(二次转载)

解决Ubuntu在虚拟机窗口不能自适应 2018年09月06日 16:20:08 起不了名儿 阅读数 855 此博文转载:https://blog.csdn.net/nuddlle/article/details/77994080(原地址) 试了很多办法这个好用 ...

tahiti_aa
昨天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部