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:
- 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 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.