Memra

Swing + threading: the Event Dispatch Thread

The single-thread rule, invokeLater, and SwingWorker — why the running GreenhouseControls threads must never block or touch the UI directly. Synthesises Module 10 with the GUI.

Swing has one rule above all others

The single-thread rule: after a Swing component is realized (shown), all code that reads or writes its state must run on the Event Dispatch Thread (EDT) — the one thread Swing uses to dispatch events and paint the screen. Swing is *not* thread-safe; calling component methods from another thread is a data race that produces intermittent, hard-to-reproduce glitches.

This creates a tension with the Greenhouse project. The controller’s event loop (Module 10: one thread per Event, suspend/resume, synchronized shared state) runs for a long time. If you run it on the EDT, the UI freezes — no repaints, no button clicks — until it finishes. So you face a two-sided constraint:

  1. Long work must run OFF the EDT (on a worker thread), or it freezes the GUI.
  2. UI updates must run ON the EDT, even when triggered by that worker thread.

Crossing back to the EDT: invokeLater

When a background thread needs to touch the UI (e.g. append a controller status line to the log), it must hand that work to the EDT:

SwingUtilities.invokeLater(() -> log.append("Event fired: LightOn\n"));

invokeLater queues the Runnable onto the EDT’s event queue and returns immediately; the EDT runs it when it next processes events. This is also how you should *start* a GUI: wrap the window construction in invokeLater from main so even the building of the UI happens on the EDT.

SwingWorker: the structured pattern

SwingWorker<T, V> packages “run long work off the EDT, publish progress, deliver the result on the EDT” into one class:

- doInBackground() runs on a worker thread — put the controller run here. Whatever it returns becomes the result of type T. - publish(V...) from the worker sends intermediate values; process(List<V>) receives them on the EDT — the safe place to update the log. - done() runs on the EDT after doInBackground finishes — call get() there to retrieve the result (or surface an exception) and re-enable buttons.

Worked example — run the controller without freezing the panel

runButton.addActionListener(e -> {
    runButton.setEnabled(false);  // on the EDT — safe
    new SwingWorker<Void, String>() {
        protected Void doInBackground() {
            controller.run();           // long work, OFF the EDT
            publish("Controller finished");
            return null;
        }
        protected void process(java.util.List<String> chunks) {
            for (String s : chunks) log.append(s + "\n");  // ON the EDT
        }
        protected void done() {
            runButton.setEnabled(true); // ON the EDT — re-enable
        }
    }.execute();
});

Trace the threads: the click handler runs on the EDT, so setEnabled(false) is safe and instant. Calling .execute() spins up a worker thread to run doInBackground()controller.run() churns there, never blocking the UI, so the window keeps repainting and the *Suspend* button stays responsive. When the worker calls publish(...), Swing schedules process(...) back on the EDT, where appending to log is legal. Finally done() runs on the EDT and re-enables *Run*. Every UI touch is on the EDT; all the slow work is off it.

NORMAL ~/memra/learn/comp-308/swing-threading-edt utf-8 LF