invokeAndWait seems to cause the application to fr

2019-07-18 18:48发布

A process which occurs in the background fires callbacks to ask various questions.

In this case the question is "is it okay to migrate your data?", so I have to ask the user. Since we have to do all Swing work on the EDT, this ends up looking like this (I only removed comments, reference to our own convenience methods and the parameters to allowMigration() - other than that, everything else is the same):

public class UserMigrationAcceptor implements MigrationAcceptor {
    private final Window ownerWindow;
    public UserMigrationAcceptor(Window ownerWindow) {
        this.ownerWindow = ownerWindow;
    }

    // called on background worker thread
    @Override
    public boolean allowMigration() {
        final AtomicBoolean result = new AtomicBoolean();
        try {
            SwingUtilities.invokeAndWait(new Runnable() {
                @Override
                public void run() {
                    result.set(askUser());
                }
            });
        } catch (InterruptedException e) {
            Thread.currentThread.interrupt();
            return false;
        } catch (InvocationTargetException e) {
            throw Throwables.propagate(e.getCause());
        }
        return result.get();
    }

    // called on EDT
    private boolean askUser() {
        int answer = JOptionPane.showConfirmDialog(ownerWindow, "...", "...",
                                                   JOptionPane.OK_CANCEL_OPTION);
        return answer == JOptionPane.OK_OPTION;
    }
}

What's happening is that in some situations, after confirming or cancelling the dialog which appears, Swing seems to get into the following state:

  • the JOptionPane is no longer visible
  • there is nothing pending on the event queue
  • the background thread is stuck inside #invokeAndWait, waiting for InvocationEvent#isDispatched() to return true.

Are we doing something wrong here, or am I looking at a bug in Swing/AWT?

The only other thing which might be worth noting is that this is the second level of modal dialogs. There is a modal dialog showing the progress of the operation and then this confirmation dialog has the progress dialog as its parent.

Update 1: Here's where the EDT is currently blocked:

java.lang.Thread.State: WAITING
      at sun.misc.Unsafe.park(Unsafe.java:-1)
      at java.util.concurrent.locks.LockSupport.park(LockSupport.java:186)
      at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2043)
      at java.awt.EventQueue.getNextEvent(EventQueue.java:543)
      at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:211)
      at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:161)
      at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:154)
      at java.awt.WaitDispatchSupport$2.run(WaitDispatchSupport.java:182)
      at java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:221)
      at java.security.AccessController.doPrivileged(AccessController.java:-1)
      at java.awt.WaitDispatchSupport.enter(WaitDispatchSupport.java:219)
      at java.awt.Dialog.show(Dialog.java:1082)
      at java.awt.Component.show(Component.java:1651)
      at java.awt.Component.setVisible(Component.java:1603)
      at java.awt.Window.setVisible(Window.java:1014)
      at java.awt.Dialog.setVisible(Dialog.java:1005)
      at com.acme.swing.progress.JProgressDialog$StateChangeListener$1.run(JProgressDialog.java:200)
      at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:251)
      at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:733)
      at java.awt.EventQueue.access$200(EventQueue.java:103)
      at java.awt.EventQueue$3.run(EventQueue.java:694)
      at java.awt.EventQueue$3.run(EventQueue.java:692)
      at java.security.AccessController.doPrivileged(AccessController.java:-1)
      at java.security.ProtectionDomain$1.doIntersectionPrivilege(ProtectionDomain.java:76)
      at java.awt.EventQueue.dispatchEvent(EventQueue.java:703)
      at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:242)
      at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:161)
      at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:154)
      at java.awt.WaitDispatchSupport$2.run(WaitDispatchSupport.java:182)
      at java.awt.WaitDispatchSupport$4.run(WaitDispatchSupport.java:221)
      at java.security.AccessController.doPrivileged(AccessController.java:-1)
      at java.awt.WaitDispatchSupport.enter(WaitDispatchSupport.java:219)
      at java.awt.Dialog.show(Dialog.java:1082)
      at javax.swing.JOptionPane.showOptionDialog(JOptionPane.java:870)

What's odd here is that the showOptionDialog() at the bottom is the migration prompt, but the Dialog#setVisible further up is the progress dialog. In other words, somehow sometimes the child dialog appears before the parent, and perhaps this is what is breaking Swing.

Update 2:

And indeed, I can make this happen in a test program without using any of our own code. Although the dialog positioning is different in the test program, it hangs in exactly the same way, only more reproducibly. gist

3条回答
叼着烟拽天下
2楼-- · 2019-07-18 19:16

I just had this same issue in my own code.

Your call to JOptionPane.showOptionDialog() never returns, because while its event dispatch loop is sitting there waiting for user input, a timer (or something else) has fired and caused another modal dialog to install its own event loop. In your stack, the culprit is JProgressDialog$StateChangeListener$1.run(), which you can then see launch its own event dispatch loop.

Until your JProgressDialog is closed, it won't exit its loop, and the earlier call to JOptionPane.showOptionDialog() will never return.

This might not be obvious if the dialog parents seem to imply a hierarchy that isn't honored by the event queue.

Two solutions might be to either avoid a modal progress dialog, or show the progress dialog immediately. Launching a modal dialog off an event is only going to be a good idea if the rest of the event thread is happy to sit back and wait for it to be closed.

查看更多
冷血范
3楼-- · 2019-07-18 19:23
  • invokeAndWait must be called out of EDT,

  • carefully with invokeAndWait, because can freeze whole Swing GUI, locked by exceptions from RepaintManager, not in all cases only GUI is created, relayout, refreshed some of methods, when repaint() is called from nested methods

  • for invokeAndWait is required to test if (EventQueue.isDispatchThread()) { / if (SwingUtilities.isEventDispatchThread()) {

  • on true from isDispatchThread() you can to result.set(askUser());) without any side effects, output is done on EDT, but is about good practicies to wrap inside invokeLater

  • I seen some usages of invokeAndWait but only on application start_up, use invokeLater() instead

查看更多
孤傲高冷的网名
4楼-- · 2019-07-18 19:25

With this kind of chaos, the first thing to suspect is that, somewhere, a Swing method is being called off the EventQueue (EDT). Swing often works very well with multiple threads, but then every once in a while this happens. Unfortunately, I know of no other way to to fix the problem than checking each and every swing method call and making sure it's on the EDT. (Note, there are one or two Swing methods that can run on other threads, such as repaint, but check the source code and Javadoc for each one to be sure.)

查看更多
登录 后发表回答