SecondaryLoop.enter() not blocking until exit() is called on the EDT

NateW :

Summary

For some reason when I call SecondaryLoop.enter() on the AWT Event Dispatch Thread (EDT), it does not wait for SecondaryLoop.exit() to be called before unblocking.

Background

Since I think SecondaryLoop is not a very well-known class, I'll give a brief overview:

In general, it is a bad idea to have any long-executing or blocking code running on the EDT because then your app will not be responsive to any events until that code terminates. The EventQueue.createSecondaryLoop() allows you to create a new event loop that will handle events, allowing you to block the EDT without loss of responsiveness. This is what swing modal dialogs use to allow you to block your EDT while you wait for the dialog to be closed, but still allow controls on the dialog itself to be able to operate.

After creating your SecondaryLoop instance, you should be able to call enter() and it should block until exit() is called.

From the docs

This method can be called by any thread including the event dispatch thread. This thread will be blocked until the exit() method is called or the loop is terminated. A new secondary loop will be created on the event dispatch thread for dispatching events in either case.

I'm not entirely sure what it means when it says "or the loop is terminated" though. That could be my issue.

Test Code

The calling the enter() method on a thread other than EDT, blocks as I would expect:

System.out.println("Enter Loop");
Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop().enter();
System.out.println("Done (we should never get here)");

Output:

Enter Loop

However, if we call it on the EDT, it blocks for about a second, but then continues on:

System.out.println("Enter Loop");
try {
    SwingUtilities.invokeAndWait(() -> Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop().enter());
} catch (InvocationTargetException | InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Done (we should never get here)");

Output:

Enter Loop
Done (we should never get here)

Per the comment by tevemadar (thanks BTW), I have updated the code to prevent any sort of possible garbage collection issue:

//Storing loop in array as a quick hack to get past the
// "final or effectively final" issue when using this in the invokeAndWait
SecondaryLoop loop[] = new SecondaryLoop[1];

System.out.println("Enter Loop");
try {
    SwingUtilities.invokeAndWait(() -> {
        loop[0] = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
        loop[0].enter();
    });
} catch (InvocationTargetException | InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Done (we should never get here)");
//Just printing this to make sure that it is used after the invokeAndWait is done. This is just
//to make sure there isn't some sort of optimization thing that is deciding that we don't
//need this anymore and allowing the loop to be garbage collected
System.out.println(loop[0]);

Output:

Enter Loop
Done (we should never get here)
java.awt.WaitDispatchSupport@2401f4c3

So, while it was a good suggestion, that does not appear to be my issue.

This seems pretty contradictory to the documentation (and the whole purpose of SecondaryLoop to me. Am I missing something?

Environment

OS: Windows 10

Java:

C:\Program Files\Java\jre8\bin>java.exe -version
java version "1.8.0_221"
Java(TM) SE Runtime Environment (build 1.8.0_221-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.221-b11, mixed mode)

Update

On a hunch, I tried adding a timer that continually adds more events to the EDT loop. It seems that adding the timer keeps the loop alive and makes it blocking:

//Add a keep alive timer which adds an event to the EDT for every 0.5 sec
new Timer(500, null).start();

System.out.println("Enter Loop");
try {
    SwingUtilities.invokeAndWait(() -> Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop().enter());
} catch (InvocationTargetException | InterruptedException e) {
    e.printStackTrace();
}
System.out.println("Done (we should never get here)");

With that code, it hangs as I expect, and if I put in some code that calls the exit() method on the loop after some time, it terminates as I would expect. So it seems that the loop must terminate itself once it has gone a certain amount of time without an event (but only if it was originally triggered from the EDT for some reason...).

I suppose I can add timers that do nothing whenever I need to use this feature, but that is definitely more of a work-around hack than a fix in my opinion.

NateW :

Figured it out (at least this specific problem, I still have some more related issues, but I'm hoping I can figure them out on my own).

I decided to start debugging around in the java source code and I realized that my thread was getting unblocked due to this segment in java.awt.EventQueue:

    /**
     * Called from dispatchEvent() under a correct AccessControlContext
     */
    private void dispatchEventImpl(final AWTEvent event, final Object src) {
        event.isPosted = true;
        if (event instanceof ActiveEvent) {
            // This could become the sole method of dispatching in time.
            setCurrentEventAndMostRecentTimeImpl(event);
            ((ActiveEvent)event).dispatch();
        } else if (src instanceof Component) {
            ((Component)src).dispatchEvent(event);
            event.dispatched();
        } else if (src instanceof MenuComponent) {
            ((MenuComponent)src).dispatchEvent(event);
        } else if (src instanceof TrayIcon) {
            ((TrayIcon)src).dispatchEvent(event);
        } else if (src instanceof AWTAutoShutdown) {
            if (noEvents()) {
                dispatchThread.stopDispatching();
            }
        } else {
            if (getEventLog().isLoggable(PlatformLogger.Level.FINE)) {
                getEventLog().fine("Unable to dispatch event: " + event);
            }
        }
    }

In my case src was AWTAutoShutdown, which resulted in my secondary loop to terminate before I called exit().

I found this article which explains that in order to make sure the event queue eventually terminates when all windows are disposed, awt determines when all components are no longer displayable and the event queue is empty, then waits for 1 second, then triggers the event with the AWTAutoShutdown class as the source which terminates the event queue and allows the JVM to terminate. That 1 second timeout is why I observed that it would hang for a little bit.

This explains why adding a timer made my code work (since I was adding an event every half second and the timeout for AWTAutoShutdown is 1 second, the event queue would be kept alive).

The use case for all of this is to basically create an EDT-safe semaphore that will allow events to keep being executed even when it's waited for on the EDT (which I use to display JavaFX dialogs from a Swing application and make it behave like a native swing modal dialog). So in my actual use case, this should work just fine (because there should always be some swing component displaying in my actual applications). However, I hadn't even actually tried out my actual use case. Being a big believer in TDD, I first focused on my JUnit tests, which didn't actually create any UI components.

So, I did a quick test with a little dummy app that does have a GUI, it works just fine. I'm just going to add my 500 ms timer into my unit tests and start it and stop it before each test.

After doing that, I'm still running into some issues with some of my tests, but my minimal verifiable example from my original question works just fine. I'll dig into the remaining test failures and hopefully figure them out on my own. If it seems to be a related issue, then I'll just add a new SO question and put a link to it here.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=305714&siteId=1