Swing + threading: the Event Dispatch Thread
◈ 7 cardsThe 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:
- Long work must run OFF the EDT (on a worker thread), or it freezes the GUI.
- 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 typeT.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 afterdoInBackgroundfinishes — callget()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.