为什么 java wait/notify 必须与 synchronized 一起使用,jvm究竟做了些什么

huangriyan 2014-11-24
   有关这个话题,网上查了很久,都没有太什么的讨论,各位能不能深入讲解一下
chainhou 2014-12-24
个人感觉,这样做关键是因为锁。
在进入synchronized的时候获取锁,退出的时候释放锁。在jvm里有相应的指令对应。
而如果没有synchronized,直接使用wait/notity时,无法确认哪个锁。比如我手里有一相苹果,我会说我把这个苹果送人了,这个时候别人需要才能拿到。如果我没有这个苹果(锁),我只是口口声声的说我要让出这个苹果,但却没有。
Now7! 2014-12-25
我们假设wait()/notify()都不需要锁。现在场景是,某条件没满足,导致我们需要block等待一下,直到条件满足为止。我们用obj来表达在等待的条件队列。

最初的尝试

我们在obj上wait(),期待其醒过来时条件是满足的。
obj.wait(); // 假设这样OK


[问题1] obj.wait()需要条件检查

不过上述代码是不可行的,因为obj.wait()会莫名其妙醒来,即条件不满足时也会醒过来。这不是我们期望的结果;所以我们需要持续检查条件,直到满足时才doSomething():
while(!条件满足) // line 1
{
    obj.wait(); // line 3
}
doSomething();


[问题2] obj.wait(), obj.notify()都需要原子性(互斥性)

假设上述代码能够工作并且block了线程T1,那么另外的线程T2可以notify()、并且更改条件,使得T1在line 1那里满足条件,从而不再wait():
更改条件为满足; // line 1
obj.notify(); // line 2

这段代码的问题在于,T1和T2的并行执行序列可能如下:
T1 line1 // T1检查条件不满足
T2 line1 // T2更改条件为满足
T2 line2 // T2.notify()
T1 line3 // T1.wait()

这将导致nofity丢失,即T2 notify()时还没有任何的wait()线程,而T1一段时间后真正wait()了却不可能收到notify()了。

所以T1的条件检查和wait()需要原子性(互斥性):
synchronized(mutex)
{
    while(!条件满足)
    {
        obj.wait();
    }
    doSomething();
}

且T2尊重这个原子性(互斥性)
synchronized(mutex)
{
   更改条件为满足;
   obj.notify();
}

这样,T1的条件检查+wait()与T2的设置条件+notify()互斥了,从而notify就不会丢失了。

[问题3] wait()需要释放mutex

其实到目前为止,还是隐含着一个问题。因为这俩线程的一种执行可能是,先T1 wait(),再T2 notify();而问题在于,T1持有mutex后block住了,T2一直无法获得mutex,从而永无可能notify()并将T1的block状态解除,就与T1形成了死锁。所以JVM在实现wait()方法时,一定需要先隐式的释放mutex,再block,并且被notify()后从wait()方法返回前,隐式的重新获得了mutex后才能继续user code的执行。要做到这点,就需要提供mutex引用给obj.wait()方法,否则obj.wait()不知道该隐形释放哪个mutex:
synchronized(mutex)
{
    while(!条件满足)
    {
        obj.wait(mutex);
        // obj.wait(mutex)伪实现
        //   [1] unlock(mutex)
        //   [2] block住自己,等待notify()
        //   [3] 已被notify(),重新lock(mutex)
        //   [4] obj.wait(mutex)方法成功返回
    }
    doSomething();
}


[最终形态] 把mutex和obj合一

其它线程API如PThread提供wait()函数的签名是类似cond_wait(obj, mutex)的,因为同一个mutex可以管多个obj条件队列。而Java内置的锁与条件队列的关系是1:1,所以就直接把obj当成mutex来用了。因此此处就不需要额外提供mutex,而直接使用obj即可,代码也更简洁:
synchronized(obj)
{
    while(!条件满足)
    {
        obj.wait();
    }
    doSomething();
}


多讨论一下sleep()和interrupt()

Hotspot里的[1]synchronized的左括号{,[2]Object.wait(),[3]AQS.acquire(),[4]Condition.await(),[5]Thread.sleep()这五者的实现在某种意义上是一致的,即基于Hotspot内部的ParkEvent.park()和Parker.park(),在linux平台上最终基于Pthread的cond_timedwait()/cond_wait();而[6]synchronized的右括号},[7]Object.notify()/notifyAll(),[8]AQS.release(),[9]Condition.signal()/signalAll(),[10]Thread.interrupt()这五者的实现在某种意义上也是一致的,即基于Hotspot内部的ParkEvent.unpark()和Parker.unpark(),在linux平台上最终基于Pthread的cond_signal()。但其它8者都需要先lock再unlock,只有[5]Thread.sleep()和[10]Thread.interrupt()不需要这样,原因是这对组合是不需要检测条件以及更改条件的,所以不需要上面[问题2]讨论的原子性(互斥性)。

结语

以上就是OP提到的wait()/notify()/notifyAll()为什么需要在synchronized(obj)包围里的原因了以及JVM做了些什么,更多是个人理解,不当之处请大家纠正。
ZHH2009 2014-12-25
这个问题需要讨论吗?大学学操作系统的课程时都会讲临界区

怎么界定临界区,synchronized把一段代码框起来不就像临界区了吗?

进到临界区的线程总会因为某种条件未满足需要睡一下觉,或等别的线程完成,
有等就有通知,多么自然的逻辑啊。
skyknowsme 2015-03-10
Java的Synchronization是Tony Hoare提出的Monitor的一个实现。

具体可以参照这篇wiki。

http://en.wikipedia.org/wiki/Monitor_(synchronization)

synchronization可以看做是已经实现了lock/unlock的基本功能,
用于互斥访问临界区,而wait和notify,notifyAll是高级一点的同步机制。

你可以这样去理解,

简单的互斥访问临界区,用synchronization足够了,
用于实现生产者消费者的例子,你会发现wait/notify很好使,直觉上就是你可以等直到你要的条件满足。

wait就会有一个wait list,里面是等待的线程,
wait就是将自己放入list,notify就是通知list中的某个去执行。
显然,这个wait list必须得互斥访问,所以是在临界区里的,
所以它需要一个synchronization。




Global site tag (gtag.js) - Google Analytics