Search code examples
javaawt

How does a FileDialog block the thread without blocking the thread?


I'm using a java.awt.FileDialog. It's a modal dialog, so when I call setVisible(true), it blocks until I pick a file or otherwise close the box.

However, there's this curious line in the documentation for setVisible:

It is OK to call this method from the event dispatching thread because the toolkit ensures that other events are not blocked while this method is blocked.

I was curious about how this worked, so I decided to test it by scheduling some other events while the dialog is visible. I used a ScheduledExecutorService to fire an event once a second.

Based on my experiment, the exact same thread is able to continue executing code, even while it's supposed to be blocked waiting for setVisible to complete. Here's the full code I used to test it.

public class Example {
  static Thread edt;
  static ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();

  static void checkThread(String source) {
    Thread thisThread = Thread.currentThread();
    String threadCheck = thisThread == edt ? "same thread" : "different thread";
    System.out.println(source + " is running on " + thisThread + " (" + threadCheck + ")");
  }

  public static void main(String[] args) throws Exception {
    EventQueue.invokeAndWait(() -> edt = Thread.currentThread());

    executor.scheduleAtFixedRate(() -> EventQueue.invokeLater(() -> checkThread("Timer")), 1, 1, TimeUnit.SECONDS);

    EventQueue.invokeLater(() -> {
      checkThread("File picker");
      FileDialog dialog = new FileDialog((Frame) null);
      dialog.setVisible(true);
      System.out.println("You picked " + dialog.getFile());
      checkThread("File picker");
      executor.shutdownNow();
    });
  }
}
  1. First, I grab a reference to the event dispatch thread.
  2. Then I start a scheduler which will run an event on the EDT every second. This event will call my checkThread function, which prints out the name of the thread and compares it to the reference I grabbed earlier.
  3. Finally, I submit another event which will show my file dialog. Before and after displaying the dialog, I call my checkThread function again.

My output looks something like this:

File picker is running on Thread[AWT-EventQueue-0,6,main] (same thread)
Timer is running on Thread[AWT-EventQueue-0,6,main] (same thread)
Timer is running on Thread[AWT-EventQueue-0,6,main] (same thread)
Timer is running on Thread[AWT-EventQueue-0,6,main] (same thread)
You picked somefile.txt
File picker is running on Thread[AWT-EventQueue-0,6,main] (same thread)

From this output, I can see that all my events ran on the same "thread". While setVisible was blocked, three other events were apparently still able to run. I even checked for referential equality, in case toString() was lying to me. So I now write "thread" in quotes, because based on this result, I can't believe this is really a thread at all.

How is it possible for this "thread" to continue executing code while it's blocked? No other thread I have ever seen, in Java or any other language, can do this. Sure, we could achieve something similar with virtual threads, but even then, each individual virtual thread still plays by the rules and stops work when it's blocked.

While digging, I came across the EventDispatchThread class, which extends Thread. Would it be correct to assume that EventDispatchThread isn't a real thread at all?

My guess is this:

  • The EventDispatchThread must be backed by more than one real thread, since it can continue to run events while one event is blocked.
  • It must be distinguishing between different types of blocking, since a simple call to Thread.sleep() does prevent other events from being dispatched.
  • Either it risks running some events in parallel, or it does some sort of synchronisation to ensure that only one of these special blocked threads can resume at once.
  • And either way, since events can apparently be interleaved, there must be some distinctly dangerous and counterintuitive implications when it comes to sharing mutable state between events.

I confess I'm a bit surprised by those last two points, since I assumed that the whole point of single-threaded UI was to ensure thread-safety and avoid any complicated synchronisation stuff.

I'd be grateful for any references or documentation that confirm or refute this guesswork. Bonus points for shedding any light on the why of it, too.

Threads are a pretty foundational part of the language, and I rely on them behaving predictably. How can this thread be both blocked and not blocked at the same time? Or, if it's not a thread, what on earth is it doing pretending to be one?


Solution

  • None of your guesses are correct. If you call 'please open me a file dialog' from the EDT, then code runs that paints a file dialog (and in the middle of that, your app is not responsive. This isn't relevant; it's fast to draw one). Then, when it is time to wait for the user to interact with it, it simply does something akin to:

    while (true) {
      UiEvent event = UiQueue.fetchNext();
      if (event.source == myself) break;
      event.handle();
    }
    // handle the event
    

    where the EDT normally does this while loop without the if part. I've oversimplified matters quite a bit, of course.

    Ordinarily that EDT handling loop occurs when your EDT-hosted code returns. However, the file open dialog can also 'host' the EDT handling queue, dropping back out once an event occurs that means the file open dialog action is ready to terminate one way or another (e.g. - the event on the EDT queue is either the 'cancel' or the 'open' button on that file dialog).