【问题标题】:Java Threading: Unexpected behavior when providing timeout argument in lock.wait()Java 线程:在 lock.wait() 中提供超时参数时出现意外行为
【发布时间】:2014-08-08 18:04:53
【问题描述】:

不幸的是,我无法提供完整的上下文,因为周围的代码太复杂了。简而言之:

我有一段代码正在等待锁:

            synchronized (lock) {
                lock.wait();
            }

按预期工作。相当简单——它获取锁,当它开始等待时释放它,另一个线程获取锁,然后通知它。

但是,一旦我提供超时,行为就会完全改变。

            synchronized (lock) {
                lock.wait(60000L);
            }

同样,应该相当简单(这在代码中的其他几个地方可以正常工作)。但是,在这种情况下,执行基本上会停止,直到发生超时。我对似乎正在发生的事情的唯一猜测是它在进入等待时没有释放锁——通知程序永远无法获得锁,所以等待会休眠直到超时。更糟糕的是,这是一个阻塞睡眠——没有其他线程能够等待锁,它会强制执行完全同步。

有人对这里可能发生的事情有任何想法吗?这是一个相当简单的功能,在任何时候嵌套同步块都没有什么奇怪的。考虑到通过不提供超时它应该无限期地等待,如果通知器本身被破坏,代码将永远挂起,但事实并非如此。仅在提供超时后才停止工作。

任何想法将不胜感激。

操作系统:OS X 10.8.5

JDK:1.6.0、1.7.0.45 和 1.7.0.67

【问题讨论】:

  • 您最好的选择是提供一个简单的测试来说明您所看到的。
  • 解决您的误解 - 如果一个线程调用 lock.wait(60000l) 它将休眠 1 分钟但也会放弃锁定,因此它不会像您担心的那样阻塞休眠。
  • notify() 电话在哪里?您可能会遇到丢失的通知。如果在调用 notify 时在 wait() 中没有其他线程已被阻塞,notify() 不会执行任何操作
  • 如果您在 while 循环中使用此代码块(如您所愿),请将同步块移出,并包装 while 循环,而不仅仅是等待部分。锁的获取和释放会产生很大的开销,建议在循环期间保持锁。
  • 是的,但我的意思是,你还没有向我们展示它。直到您向我们展示了一个完整的示例(例如,wait() 是如何发生的 notify() 是如何发生的),那么我们只能猜测哪里出了问题。

标签: java multithreading mutex


【解决方案1】:

您的示例没有显示 wait() 调用周围的 while() 循环。这表明您可能不完全理解等待和通知的用例。这是一个例子:

// This object is used to synchronize *EVERY* method
// that can change the value of count.
final Object lock = new Object();

int count;

void waiter() {
    synchronized(lock) {
        while(count <= 0) {
            lock.wait();
        }
        //do something that you are only allowed to do
        //when count > 0.
    }
}

void notifier() {
    synchronized(lock) {
        count++;
        if (count >= 0) {
            lock.notify();
        }
    }
}

[编辑:添加了这一段,感谢 Nathan Hughes 提醒我...] wait() 调用处于循环中,因为在锁定后等待()线程仍然需要重新获取锁定通知:如果线程 A 正在等待条件变为真,而线程 B 使条件变为真并调用 notify();不能保证线程 C 不会首先获得锁,并在 wait() 调用能够返回之前再次使条件为 false。

此外,即使对象没有被通知,wait() 也可以返回(这称为“虚假唤醒”)。

要等待的条件在代码中是明确的(即 count > 0)。

除非在用于 wait() 和 notify() 调用的同一个锁对象上同步,否则不会更改要等待的条件。

【讨论】:

  • 在不尝试进行架构讨论的情况下,让我们假设目前我什至不在乎它是否会早起。问题是它根本没有醒来。通知永远不会发生 - 据我所知,执行通知的线程无法获取锁,即使它应该在调用 wait() 时释放。
  • @Apropos 有一些方法误用 wait() 和 notify() 会导致一种称为“丢失通知”的错误。在没有看到更多代码的情况下,我只能猜测这可能是您的问题。如果您没有等待()明确条件,并且您没有锁定 每个 可以更改条件的代码,那么您的程序可能容易丢失通知。
  • 如果有帮助,我已经投入了日志记录,我可以看出通知器甚至无法进入同步块(似乎无法获取锁)。我希望我可以为您提供更多代码。它不是在等待明确的条件(因此可能容易丢失通知),但这似乎不是在这种情况下发生的事情。
【解决方案2】:

无论您是否提供超时,对象的 wait 方法都会释放当前线程持有的锁,正如 John 所评论的那样。

使用您给出的代码并根据您对场景的描述,我的猜测是,在执行 lock.wait(60000L) 的那一刻,JVM 会释放对象上的锁,同时处于可运行/运行状态的任何其他线程可能被拾起,如果它们在同一个对象上同步,那么它们可能会在您的通知线程获取锁之前获取锁。

这种行为很难调试,因为它依赖于 JVM 分析器来选择应该运行的线程。因此,正如您在执行 lock.wait(60000L) 时所解释的那样,通知程序线程不必总是单独获取 common object 上的锁。如果有任何其他线程也在等待公共对象,它可以很好地获得锁,最终导致通知线程无法获得锁,因此 lock.wait(60000L) 超时。

【讨论】:

  • 当然不仅仅是通知线程可能正在获取锁(还有其他线程也可能正在获取等待等待的锁),但如果有问题(甚至是间歇性的)能力对于获取锁的通知器,我肯定会在不提供超时时看到它——但是,事实并非如此。通知线程能够按预期获取锁,直到我添加超时。它始终在没有超时的情况下工作,并且在超时时始终失败。
【解决方案3】:

当你使用 lock.wait(..) 时,你必须使用 lock.notify()lock.notifyAll() .确保在逻辑中有意义的地方使用它,它会在超时之前“唤醒”锁(考虑到你输入的超时值就足够了)。这是它的一些使用指南,我希望它有用:http://www.javamex.com/tutorials/wait_notify_how_to.shtml

【讨论】:

  • 对不起。你看过我的帖子吗?我知道它应该如何工作。就像我说的,它在不提供超时时按预期工作。另一个线程通知锁定并且代码继续按应有的方式执行。提供超时时,通知不再发生,因为其他线程似乎由于某种原因无法获取锁。我进一步提到它是如何在代码的其他地方按预期工作的。编辑:为了进一步澄清,如果没有调用 notify,代码将锁定且不提供超时,因为它会无限期地等待。
  • 等待不会“保留”锁,在等待时它允许其他线程获取锁,这就是为什么当然可以调用 notify() 的原因。检查你的代码的其他部分是写在 synchronized(lock)..some 其他地方它可能保持锁,而不是'wait()'。
  • 只是一点旁注......'不保持锁定'是与 wait() 和 Thread.sleep() 的主要区别。最后一个确实持有锁,直到它超时表现得像你描述的那样。 (即在它睡觉时没有其他人可以进入那个锁)。
  • 是的,我知道。 wait() 应该放弃锁。我只是说它表现得好像是阻塞睡眠而不是等待。我对这种行为的唯一解释是它不是,即使它应该是。如果真的不是,那么我不知何故触发了一个 JVM 错误(这似乎不太可能)。对于实际发生的事情,我可能完全错了,但我没有其他解释为什么仅仅提供超时会导致通知停止。
  • @AlécioCarvalho,没有。 Object.wait(n) 和 Thread.sleep(n) 之间的主要区别在于,如果其他程序员试图理解您的代码,他们会认为 Thread.sleep(n) 意味着它正在延迟在一段时间内,他们会认为 foobar.wait(n) 意味着它正在等待其他线程某事。
猜你喜欢
  • 2016-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多