为什么必须等待()始终处于同步块
我们都知道,为了调用Object.wait()
,这个调用必须放在synchronized块中,否则抛出IllegalMonitorStateException
异常。 但是这个限制的理由是什么呢? 我知道wait()
释放监视器,但为什么我们需要通过使特定的块同步来显式获取监视器,然后通过调用wait()
释放监视器?
如果可以在同步块之外调用wait()
,保留它的语义,挂起调用者线程有什么潜在的危害?
wait()
只有在notify()
wait()
时才有意义,所以它总是关于线程之间的通信,需要同步才能正常工作。 有人可能会争辩说,这应该是隐含的,但这不会真的有帮助,原因如下:
在语义上,你永远不要wait()
。 你需要一些条件,如果不是,你就等着。 所以你真正做的是
if(!condition){ wait(); }
但条件是由一个单独的线程设置,所以为了使这个工作正确,你需要同步。
还有更多的事情是错误的,只是因为你的线程退出等待并不意味着你正在寻找的条件是正确的:
-
你可以得到虚假的唤醒(意味着一个线程可以从没有收到通知的等待中唤醒),或者
-
条件可以设置,但是第三个线程在等待线程唤醒(并重新获取显示器)的时候再次使条件变为false。
为了处理这些情况,你真正需要的总是这样的一些变化:
synchronized(lock){ while(!condition){ lock.wait(); } }
更好的是,不要混淆同步原语,而要使用java.util.concurrent
包提供的抽象。
如果可以在同步块之外调用
wait()
,保留它的语义,挂起调用者线程有什么潜在的危害?
我们来举例说明,如果wait()
可以在具有一个具体例子的同步块之外被调用,那么我们会遇到什么问题。
假设我们要实现一个阻塞队列(我知道,已经有一个在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();
buffer.add(data); notify();
-
消费者线程现在将调用
wait()
(并错过刚刚调用的notify()
)。 -
如果不幸的话,生产者线程不会产生更多的
give()
因为消费者线程永不会被唤醒,而且我们有一个死锁。
一旦你明白这个问题,解决方案是显而易见的:总是执行give
/ notify
和isEmpty
/ wait
自动。
没有进入细节:这个同步问题是普遍的。 正如Michael Borgwardt所指出的,wait / notify是关于线程之间的通信的,所以你总是会得到与上面描述的类似的竞争状态。 这就是为什么“只在等待内同步”规则强制执行。
@Willie发布的链接中的一段很好地总结了这一点:
你需要绝对保证服务员和通知者对谓词的状态达成一致。 服务员在进入睡眠之前稍微检查一下谓词的状态,但是当它进入睡眠状态时,谓词的正确性依赖于它。 这两个事件之间有一段脆弱的时期,可能会打破这个计划。
生产者和消费者需要同意的谓词在上面的例子buffer.isEmpty()
。 协议是通过确保等待和通知在synchronized
块中执行来解决的。
这篇文章在这里被重写为一篇文章: Java:为什么等待必须在同步块中调用
@滚球是正确的。 wait()
被调用,所以当这个wait()
调用发生时,线程可以等待一些条件发生,线程被迫放弃它的锁。
放弃一些东西,你需要先拥有它。 线程需要先拥有锁。 因此需要在synchronized
方法/块中调用它。
是的,如果您没有在synchronized
方法/程序块中检查条件,我同意所有上述有关潜在损害/不一致的答案。 然而正如@ shrini1000指出的那样,只要在synchronized块中调用wait()
就不会避免这种不一致的发生。
这是一个很好的阅读..
如果您在wait()
之前未同步,则可能导致的问题如下所示:
- 如果第一个线程进入
makeChangeOnX()
并检查while条件,并且它是true
(x.metCondition()
返回false
,意味着x.condition
是false
),所以它会进入它。 然后,在wait()
方法之前,另一个线程转到setConditionToTrue()
并将x.condition
设置为true
并将notifyAll()
。 - 那么只有在那之后,第一个线程才会进入他的
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; } }
直接从这个 java的oracle教程:
当线程调用d.wait时,它必须拥有d的内部锁 – 否则会引发错误。 在同步方法内调用等待是获取内部锁定的简单方法。
这基本上与硬件架构(即RAM和高速缓存 )有关。
如果不同时使用wait()
或notify()
synchronized
,另一个线程可能会进入同一个块,而不是等待监视器输入。 而且,当访问一个没有同步块的数组时,另一个线程可能不会看到它的变化…实际上,另一个线程在x级缓存中已经有一个数组的副本时 不会看到任何变化即第一/第二/第三级高速缓存)的线程处理CPU内核。
但是同步块只是奖牌的一面:如果实际上从非同步上下文访问同步上下文中的对象,即使在同步块内,对象也不会被同步,因为它拥有自己的副本对象在其缓存中。 我在这里写了关于这个问题: https : //stackoverflow.com/a/21462631 当锁持有非最终对象,该对象的引用是否仍然被另一个线程更改?
而且,我确信x级缓存是造成大多数不可重现的运行时错误的原因。 这是因为开发人员通常不会学习底层的东西,比如CPU如何工作,或者内存层次如何影响应用程序的运行: http : //en.wikipedia.org/wiki/Memory_hierarchy
它仍然是一个谜,为什么编程类首先不以内存层次结构和CPU架构开始。 “你好世界”在这里不会有帮助。 ;)
当你从一个对象t调用notify()时,java会通知一个特定的t.wait()方法。 但是,java如何搜索并通知一个特定的等待方法。
java只查找由对象t锁定的同步代码块。 java不能搜索整个代码来通知一个特定的t.wait()。