SwingWorker runs background work separately from the Swing event dispatch thread and delivers completion callbacks to the event thread.
Java SwingWorker: background work and event-thread publication
This program targets Java 8 without preview flags. Use a full JDK for compiler-based examples.
The UI thread is an ownership boundary
A desktop receipt viewer should not parse a large import inside a button listener. That listener runs on the event dispatch thread. Blocking it prevents repainting and input handling, so the window can look frozen even when the parser is making progress.
The background task returns a result rather than modifying a Swing label directly. done obtains the completed result and updates the model on the event thread. The fixture uses an atomic reference only to transfer its observed label back to the test thread; the Swing label itself remains event-thread-owned.
Completion and waiting are different operations
Calling get on a still-running worker from the event thread recreates the blocking problem. done is different because the task has already completed; failure still needs handling because get can throw ExecutionException or InterruptedException. Preserve interruption when catching the latter.
This headless program verifies background execution, event-thread completion and the label value without opening a window. It is a Swing threading fixture, not a full desktop application. A real application still needs cancellation, input disabling and an explicit failure display.
Working program
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
public class ReceiptViewerWorker {
public static void main(String[] args) throws Exception {
AtomicReference<String> observed = new AtomicReference<>();
CountDownLatch completed = new CountDownLatch(1);
SwingUtilities.invokeAndWait(() -> {
JLabel label = new JLabel("Waiting");
SwingWorker<Integer, Void> worker = new SwingWorker<Integer, Void>() {
protected Integer doInBackground() {
if (SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("Work on UI thread");
return 6;
}
protected void done() {
try {
if (!SwingUtilities.isEventDispatchThread()) throw new IllegalStateException("Completion off UI thread");
label.setText("Imported="+get()); observed.set(label.getText());
} catch (InterruptedException failure) { Thread.currentThread().interrupt(); observed.set("interrupted"); }
catch (Exception failure) { observed.set("failed"); }
finally { completed.countDown(); }
}
};
worker.execute();
});
if (!completed.await(5, TimeUnit.SECONDS)) throw new IllegalStateException("Completion deadline");
System.out.println(observed.get());
}
}Output
Imported=6Costs and boundaries
The fixture does constant background work and retains one label and one worker. Application cost follows the actual parser and its result, not SwingWorker itself. Publishing thousands of small UI updates can overwhelm the event queue; coalesce progress instead of repainting for every row. A timeout in this test bounds its wait, not the latency of every desktop operation.
Common Mistakes
- Do not mutate Swing components from doInBackground.
- Do not wait for unfinished work on the event thread.
- Cancellation needs cooperation from the background operation.
Read next
Cancellation and waiting, Executor boundaries, Shared state.
