首页 文章

为什么wait()始终处于同步块中

提问于
浏览
231

我们都知道,为了调用Object.wait(),此调用必须放在synchronized块中,否则抛出IllegalMonitorStateException . 但 what's the reason for making this restriction? 我知道 wait() 释放监视器,但为什么我们需要通过使特定块同步显式获取监视器,然后通过调用 wait() 释放监视器?

如果可以在同步块之外调用 wait() ,保留它的语义 - 暂停调用程序线程,可能造成的损害是什么?

8 回答

  • 0

    只有 notify()wait() 才有意义,所以它始终是线程之间的通信,需要同步才能正常工作 . 有人可能会认为这应该是隐含的,但由于以下原因,这不会有任何帮助:

    从语义上讲,你永远不会只是 wait() . 你需要一些条件来满足,如果不是,你就等到它 . 所以你真正做的是

    if(!condition){
        wait();
    }
    

    但是条件是由一个单独的线程设置的,所以为了使这个工作正常,你需要同步 .

    还有一些问题,只是因为你的线程退出等待并不意味着你正在寻找的条件是真的:

    • 您可以获得虚假的唤醒(意味着线程可以在没有收到通知的情况下从等待中唤醒),或者

    • 条件可以设置,但是第三个线程在等待线程唤醒(并重新获取监视器)时再次使条件为false .

    为了处理这些情况,你真正需要的是它的一些变化:

    synchronized(lock){
        while(!condition){
            lock.wait();
        }
    }
    

    更好的是,不要混淆同步原语并使用 java.util.concurrent 包中提供的抽象 .

  • 3

    如果可以在同步块之外调用wait(),保留它的语义 - 暂停调用程序线程,那么可能造成的损害是什么?

    让我们举例说明如果 wait() 可以在带有 concrete example 的同步块之外调用,我们会遇到什么问题 .

    假设我们要实现一个阻塞队列(我知道,API中已有一个:)

    第一次尝试(没有同步)可能看起来像下面的行

    class BlockingQueue {
        Queue<String> buffer = new LinkedList<String>();
    
        public void give(String data) {
            buffer.add(data);
            notify();                   // Since someone may be waiting in take!
        }
    
        public String take() throws InterruptedException {
            while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
                wait();
            return buffer.remove();
        }
    }
    

    这是可能发生的事情:

    • 消费者线程调用 take() 并看到 buffer.isEmpty() .

    • 在消费者线程继续调用 wait() 之前,生成器线程出现并调用完整的 give() ,即 buffer.add(data); notify();

    • 消费者线程现在将调用 wait() (并且错过刚刚调用的 notify() ) .

    • 如果运气不好, 生产环境 者线程不会产生更多 give() ,因为消费者线程永远不会醒来,而且我们有死锁 .

    一旦理解了问题,解决方案就很明显了:使用 synchronized 确保 isEmpty 永远不会在 isEmptywait 之间调用 .

    没有详细说明:此同步问题是通用的 . 正如Michael Borgwardt指出的那样,wait / notify是关于线程之间的通信的,所以你总是会遇到与上述类似的竞争条件 . 这就是强制执行“仅在内部同步”规则的原因 .


    link posted by @Willie中的一段很好地总结了它:

    您需要绝对保证服务员和通知者同意谓词的状态 . 服务员在进入睡眠状态之前稍微稍微检查一下谓词的状态,但这取决于谓词在进入睡眠状态时的正确性 . 这两个事件之间存在一段时间的漏洞,这可能会破坏该计划 .

    生产环境 者和消费者需要达成一致的谓词在上面的例子中是 buffer.isEmpty() . 并且通过确保在 synchronized 块中执行等待和通知来解决协议 .


    这篇文章已被重写为文章:Java: Why wait must be called in a synchronized block

  • 0

    @Rollerball是对的 . 调用 wait() ,以便线程可以等待发生此 wait() 调用时发生某种情况,线程被强制放弃锁定 .
    要放弃一些东西,你需要先拥有它 . 线程首先需要拥有锁 . 因此需要在 synchronized 方法/块中调用它 .

    是的,如果您未在 synchronized 方法/区块内检查条件,我同意上述有关潜在损害/不一致的所有答案 . 但正如@ shrini1000指出的那样,只是在synchronized块中调用 wait() 将不会避免发生这种不一致 .

    Here is a nice read..

  • 207

    如果在 wait() 之前未进行同步,则可能导致的问题如下:

    • 如果第一个线程进入 makeChangeOnX() 并检查while条件,并且 truex.metCondition() 返回 false ,表示 x.conditionfalse ),那么它将进入它 . 然后在 wait() 方法之前,另一个线程转到 setConditionToTrue() 并将 x.condition 设置为 truenotifyAll() .

    • 然后只有在那之后,第一个线程将进入他的 wait() 方法(不受之前发生的 notifyAll() 影响) . 在这种情况下,第一个线程将继续等待另一个线程执行 setConditionToTrue() ,但这可能不会再次发生 .

    但是如果在改变对象状态的方法之前放置synchronized,则不会发生这种情况 .

    class A {
    
        private Object X;
    
        makeChangeOnX(){
            while (! x.getCondition()){
                wait();
                }
            // Do the change
        }
    
        setConditionToTrue(){
            x.condition = true; 
            notifyAll();
    
        }
        setConditionToFalse(){
            x.condition = false;
            notifyAll();
        }
        bool getCondition(){
            return x.condition;
        }
    }
    
  • 254

    我们都知道wait(),notify()和notifyAll()方法用于线程间通信 . 为了摆脱错过的信号和虚假的唤醒问题,等待线程总是在某些条件下等待 . 例如 . -

    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
    

    然后通知线程集wasNotified变量为true并通知 .

    每个线程都有自己的本地缓存,所以所有的更改都先写入,然后逐渐升级到主内存 .

    如果在synchronized块中没有调用这些方法,则wasNotified变量将不会刷新到主内存中并且将存在于线程的本地缓存中,因此等待线程将继续等待信号,尽管它是通过通知线程重置的 .

    为了解决这些类型的问题,这些方法总是在synchronized块内调用,这确保了当synchronized块启动时,所有内容都将从主内存中读取,并在退出synchronized块之前刷新到主内存中 .

    synchronized(monitor) {
        boolean wasNotified = false;
        while(!wasNotified) {
            wait();
        }
    }
    

    谢谢,希望它澄清 .

  • 0

    直接来自this java oracle教程:

    当线程调用d.wait时,它必须拥有d的内部锁 - 否则会引发错误 . 在synchronized方法中调用wait是获取内部锁的一种简单方法 .

  • 2

    这基本上与硬件架构有关(即 RAMcaches ) .

    如果您不将 synchronizedwait()notify() 一起使用,则另一个线程可以进入同一个块而不是等待监视器输入它 . 此外,当例如在没有同步块的情况下访问数组,另一个线程可能看不到对它的更改...实际上当另一个线程已经在x级缓存中具有该数组的副本时,另一个线程将看不到它的任何更改(aka 1st / 2nd线程处理CPU核心的/第三级缓存 .

    但是同步块只是奖牌的一面:如果您实际从非同步上下文访问同步上下文中的对象,则即使在同步块内,该对象仍然不会同步,因为它拥有自己的副本对象在其缓存中 . 我在这里写了这个问题:https://stackoverflow.com/a/21462631When a lock holds a non-final object, can the object's reference still be changed by another thread?

    此外,我是因为开发人员通常不会工作,或者内存层次结构如何影响应用程序的运行:http://en.wikipedia.org/wiki/Memory_hierarchy

    为什么编程类不首先从内存层次结构和CPU架构开始,这仍然是一个谜 . “Hello world”在这里无济于事 . ;)

  • 11

    当您从对象t调用notify()时,java会通知特定的t.wait()方法 . 但是,java如何搜索并通知特定的等待方法 .

    java只查看由对象t锁定的同步代码块 . java无法搜索整个代码来通知特定的t.wait() .

相关问题