Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java SwingWorker: background work and event-thread publication

Last updated: 29 Sept 20264 min read
tutorial
IntermediateBy AITrove Editorial

SwingWorker runs background work separately from the Swing event dispatch thread and delivers completion callbacks to the event thread.

Download Java source kit

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

Java
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

Output
Imported=6

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

java
swing-workers
Storage details