Java对象同步。为什么这不会出现死锁?

4
下面的结构可以正常工作并实现我想要的功能,但我希望了解为什么它不会产生死锁。
下面的示例确保用户在继续之前已经点击了弹出的JOptionPane框(在EDT上)。
package waitexample;

import javax.swing.JOptionPane;
import javax.swing.SwingUtilities;

public class WaitExample {

    public static void main(String[] args) throws InterruptedException {
        System.out.println("starting");
        Object myWaiter = new Object();

        SwingUtilities.invokeLater(() -> {
            System.out.println("invoked");            
            JOptionPane.showConfirmDialog(null, "Message", "Title", JOptionPane.YES_NO_OPTION);
            synchronized (myWaiter) {
                System.out.println("calling notify");
                myWaiter.notify();
                System.out.println("notified");
            }
        });

        synchronized (myWaiter) {
            System.out.println("waiting");
            myWaiter.wait();
            System.out.println("done waiting");
        }

        System.out.println("ending main()");
    }
}

但是从以下输出来看,似乎我正在与不同的线程同时进入synchronized(myWaiter)块:

starting
waiting
invoked
calling notify
notified
done waiting
ending main()

为什么这个程序没有死锁?

2
在锁上等待会释放该锁,并在唤醒后重新获取它。 - tkausl
2个回答

2
如果等待线程在持有锁时进入休眠状态,那么就会发生死锁。但实际上并不是这样的,参见Object#wait的API文档,特别是第二段:

导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法。换句话说,此方法的行为就像简单地执行wait(0)调用一样。

当前线程必须拥有此对象的监视器。线程释放此监视器的所有权,并等待,直到另一个线程通过调用notify方法或notifyAll方法唤醒等待此对象监视器上的线程。然后线程等待,直到它可以重新获得监视器的所有权并恢复执行。

与一个参数版本一样,中断和虚假唤醒是可能的,因此应该始终在循环中使用此方法:

 synchronized (obj) {
     while (<condition does not hold>)
         obj.wait();
     ... // Perform action appropriate to condition
 }

这个方法只能由拥有该对象监视器的线程调用。请参见notify方法,了解线程成为监视器所有者的方式。

当您的主线程进入等待方法时,它会释放在进入同步块时获取的锁,因此该锁可供EDT线程获取。在EDT上执行的Runnable发现锁可用并获取它,在其上调用notify,打印出其消息并退出同步块,释放锁。此时,主线程在退出等待方法之前获取锁,然后在退出块时释放锁。

请注意,仅因为wait退出并不意味着发生了通知,wait可能在没有发生任何通知的情况下退出(这是API文档中提到的虚假唤醒)。此外,尽管在这种情况下,您的通知代码正在等待UI控件输入,因此主线程将首先等待,但总体而言,您不希望依赖于等待在通知之前发生:如果通知确实先发生,则等待可能无限期地进行。您可以通过在循环中调用wait(如上面的文档中建议的那样),检查由通知代码设置的某个条件来解决这两个问题。


谢谢您提供的详细信息。希望我能够接受两个答案。 :-) - kmort

2
引用wait文档的内容:
线程释放该监视器的所有权并等待,直到另一个线程通过调用notify方法或notifyAll方法通知正在等待此对象监视器的线程唤醒。然后,线程等待,直到它可以重新获得监视器的所有权并恢复执行。
这解释了上述内容。
starting
waiting
invoked <- could have appeared before or after waiting

引用notify的文档:

唤醒的线程在当前线程释放此对象的锁之前无法继续进行。 唤醒的线程将像通常情况下一样与任何其他可能正在积极竞争以在此对象上同步的线程竞争; 例如,唤醒的线程在成为锁定此对象的下一个线程方面没有可靠的特权或劣势。

这解释了什么。

calling notify
notified

因为此时EDT线程放弃同步块并释放锁,因此接下来的代码会被执行:

done waiting
ending main()

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