理解wait()和notify()的必要性

7
我试图通过使用wait()notify()来了解在访问共享资源或依赖它们的状态时实现线程的必要性。
我看到的想法是监视对象并等待其可用性,在使用后释放它们,以使它们对其他线程/方法可用,但为什么需要这些方法,而不是将相关对象声明为静态易失性,以便其他线程在不调用这些方法的情况下了解状态的变化?
例如,在餐厅里有两位厨师。其中一位是好厨师(更好的烹饪质量...),携带布尔值isGoodCook = true,而第二位厨师是坏厨师,携带布尔值isGoodCook = false
同时只有一个厨师可以使用设备进行烹饪。坏厨师烹饪一段特定时间(= cookingTime),而好厨师偶尔进入厨房接管坏厨师的烹饪任务。好厨师在烹饪过程中永远不能被打断,并且一旦开始就会一直烹饪整个cookingTime。
(当好厨师接替烹饪餐点的部分时,坏厨师停止烹饪。)
好厨师停止烹饪餐点后,坏厨师必须重新开始准备餐点的任务。
private boolean cooking; //is equipment used at the moment
private boolean isGoodCook;
private boolean cookingDesire; //indicating to chef to stop cooking
private int cookingTime;


public CookingTask(boolean isGoodCook, int cookingTime)
{
    this.isGoodCook = isGoodCook;
    this.cookingTime = cookingTime;
}

public void run()
{  
    if(isGoodCook)
    {
        //notify actual cook to stop cooking
        cookingDesire = true; 
    }
    //wait til equipment to cook
    //is available
    while(cooking)
    {
        try 
        {
            wait();
        } catch (InterruptedException e) 
        {
            e.printStackTrace();
        }
    }
    //reserve equipment
    cooking = true;
    cookingDesire = false;
    //notify other threads (= bad cook)
    notifyAll();
    startCooking();
}

private void startCooking()
{
    for(int i = 0; i < cookingTime; cookingTime--)
    {
        try 
        {
            Thread.sleep(1000);
            //if good chef comes in
            if(cookingDesire)
            {
                //bad chef starts to pause
                startBreak();
            }
        }
        catch (InterruptedException e) 
        {
            e.printStackTrace();
        }
    }
    cooking = false;
}

public void startBreak()
{
    //bad chef stops cooking
    cooking = false;
    notifyAll();
    //measure break time of bad chef
    long stopCookingTime = System.currentTimeMillis();
    while(cookingTime > 0 && cooking)
    {
        try 
        {
            wait();
        } catch (InterruptedException e) 
        {
            e.printStackTrace();
        }
    }
    int pausedTime = toIntExact((System.currentTimeMillis() - stopCookingTime)/1000);
    //calculate remaining cookingTime
    cookingTime -= pausedTime;
    cooking = true;
    notifyAll();
}

也许有人有时间阅读并简要概述我对多线程监控/wait()notify()的误解,我将不胜感激!

1
因为您可以将值类型或对象的引用标记为volatile,而不是对象本身。 - Giovanni Caporaletti
完全有道理,让事情变得清晰明了。非常感谢! - freeprogramer233
volatile关键字提供了一种较弱的线程安全形式。它保证了4字节(及更短)数据类型的可见性,但不提供任何形式的互斥或原子性。当需要完全控制同步时,wait()和notify()是低级别的工具,可以实现它。大多数Java程序员不会使用wait()或notify(),而是使用更健壮且更简单易用的Java并发API。 - scottb
4个回答

4

静态意味着一个类的所有对象共享该数据。那么,您认为如何使用静态字段来说明特定线程对象的状态?

我猜可以摆脱wait/notify;以一种方式,使一个线程必须查询其他线程的属性。但这意味着“活动”:那个“等待”的线程必须进行轮询。当然,你不能一直轮询,所以你会希望它在一定时间内休眠。这几乎就像是等待,但更加复杂,因为你必须通过编写代码来管理所有微妙的细节。

使用wait/notify,您有一个推模型。如果一个线程需要等待;您告诉它这样做;然后,在时机到来时,它将被唤醒。这是一个非常清晰,直接的语义。

因此,当您提出不同的模型来解决这个问题时,您真的必须证明您的模型实现了相同的目标;并且超越了该模型的额外好处。


其实,我非常肯定 "等待"-"通知"方法绝对有其存在的合理性,只需要确认一下才能清楚地理解它。谢谢你的答复。 - freeprogramer233

3
将相关对象声明为静态易失性,以便其他线程在不调用这些方法的情况下了解状态变化的目的是什么?
静态和易失性的目的与线程提供的等待-通知机制在状态更改或满足条件时完全不同。
静态-表示字段/方法与类而不是实例级别关联,并且不需要创建对象即可使用它们。
易失性-指示JVM始终读取此字段的最新值,即保证易失性变量对线程的更改可见性并避免缓存一致性问题。
谈到等待和通知,它是线程使用的通信机制,并且我们将从生产者-消费者示例中查看它。
生产者将需要由消费者消耗/完成的任务放入队列中。
从等待任务出现在队列(空队列)的消费者开始。生产者放置任务,随后通知任何正在等待的消费者。这将唤醒消费者并开始处理任务。否则,消费者必须持续轮询队列以获取任务。
从生产者的角度来看,它会不断将任务放入队列中,直到队列满了,然后生产者等待,直到队列中有可用空间。当消费者接收任务并在队列中释放一些空间时,可以通知生产者。如果没有这个机制,生产者就必须定期轮询队列以获取可用空间。
因此,wait-notify是线程之间用于相互通信的通信机制,用于任何条件/状态变化。
此外,在wait和notify中,JMM提供了具有happens-before关系的内存可见性保证。
类似地,JMM保证对易失字段的写入发生在随后对该字段的每次读取之前,但不要将其与线程提供的wait-notify机制混淆。
下面是来自Java revisited link的图像,显示使用wait notify的生产者-消费者模式。绿色虚线调用notify方法以在满足条件时唤醒等待线程。如果您对相关代码感兴趣,可以查看link

Java wait-notify with Producer consumer pattern


在您的生产者和消费者示例中,每个生产者和消费者都有一个线程,您将如何声明队列及其剩余空间/大小(变量)? - freeprogramer233
在我们的示例队列中,@freeprogramer233 可以被声明为一个数组,并且生产者和消费者都从同一个数组中工作。因此,当队列为空时,消费者会等待(空队列场景),直到至少有一个任务可以处理;而生产者则会等待数组中有空闲空间(队列满场景)。 - Madhusudana Reddy Sunnapu
请注意,volatile关键字还具有相当强的可见性保证,因为在同一变量的volatile写入和随后的volatile读取之间存在“happens-before”关系。@MadhusudanaReddySunnapu - biziclop
@biziclop同意,向volatile字段的写入发生在对该字段的每个后续读取之前,这是答案中值得注意的一点。 - Madhusudana Reddy Sunnapu

1

由于两者的目的不同,让我们了解核心差异。

static:所有对象都有一个变量副本。

volatile:从主存中获取变量值,而不是线程缓存。

在许多应用程序中,您需要为对象拥有副本,而不是所有对象都使用类级别副本的情况下,如果您的建议是处理多线程应用程序的方式,就不应受到限制,即使用static volatile

现在,让我们来看看单独使用volatile variable时,在没有static的情况下。从主内存获取最新值是可以的。但是,如果多个线程正在更改该值,则无法保证一致性。

以您的银行帐户为例。假设您的帐户余额为50美元。一个线程正在从deposit()方法中添加值。另一个线程正在从withdrawal()方法中扣除该值。

Thread 1:从主存而不是缓存中获取余额的值为50美元。存款发生了25美元,余额现在变为75美元。假设存款未同步,并且未使用wait()和notify()。

public void deposit(){
     //get balance : 50
     // update : 75
     // print  : 25 ( if withdrawal completes before print statement and 
        after get balance statement  
}

线程2: 从主内存中获取了余额为50美元的值,而不是缓存。提取了25美元并且余额现在变为了25。

public void withdrawal(){
     //get balance : 50
     // update : 25
     // print : 25
}

仅将变量设置为volatile并不能帮助。您需要同步数据以保持一致性。

请查看下面的SE问题,以更好地理解这些概念:

Java中volatile和synchronized的区别

Java中Volatile与Static的比较

静态变量与Volatile的比较


非常感谢您详细的回答。 - freeprogramer233

1

使用锁和/或volatile变量来执行“wait”和“notify”所设计的同步任务可能是可能的,但这不是高效的。作为预期用法的简单示例,考虑一个消息队列和一个处理消息的线程。如果该线程没有处理消息且队列为空,则该线程将无法执行任何有用的操作。

可以拥有一个只是获取锁,检查队列是否为空,如果不是则短暂地释放锁,然后重新获取它,再次检查队列是否为空等待待处理消息的消息处理线程,但此类线程需要花费大量时间检查未处理的消息队列,而在此期间CPU有更加有用的事情需要做。

“等待”的最简单理解方式是,它向Java虚拟机提供了一个指示,即除非有其他线程通过“notify”发出表示可能发生了一些有趣的事情,否则该线程已经确定没有任何有用的事情需要做。虚拟机不需要对此指示做任何事情,但在大多数情况下,它将防止执行“wait”的线程在等待对象接收到“notify”之前获得任何更多的CPU时间。该线程甚至可能在此之前就会获得CPU时间[例如,在小型嵌入式系统中运行的JVM可能不会费心跟踪线程正在等待的对象,但可以合法地使“wait”挂起线程,直到给任何对象发出下一个“notify”],但是,让一个线程在实际需要之前不必要地几次短暂地获得CPU时间,比让一个线程不必要地持续获得CPU时间要好得多。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接