Skip to content

TD-Threads — Concurrency: the EDT & SwingWorker

← Part 2 Overview

S7 Inf A3 — Java for Graphical and Mobile Programming
Stéphane Derrode — Centrale Lyon
Part 2 — GUI with Swing
Duration: 2h · Continue the td5 Maven project (or create td-threads)


Objectives

By the end of this session you will be able to:

  • Explain what the Event Dispatch Thread (EDT) is and why all Swing UI work happens on it
  • Animate a panel without blocking, using a javax.swing.Timer
  • Recognise — and reproduce on purpose — a frozen window caused by a long task on the EDT
  • Move long work off the EDT with SwingWorker, reporting progress with publish/process and finishing in done()
  • Connect this rule to Android's main thread rule (CM6) and to a "computing move…" indicator you will reuse in BE1

Part 1 — The Event Dispatch Thread (20 min)

1.1 One thread for the whole UI

Everything you did in TD4 and TD5 — clicks, repaint(), button callbacks, label updates — runs on a single thread called the Event Dispatch Thread (EDT). Swing is not thread-safe: only the EDT is allowed to touch components.

That is exactly why every main() so far wrapped the window creation in:

SwingUtilities.invokeLater(() -> new MyFrame().setVisible(true));

invokeLater says: "run this on the EDT when it is free." The EDT spends its life in a loop:

forever:
    take the next event from the queue   (click, key, repaint request…)
    run its listener / paint code

While a listener is running, no other event is processed — no repaint, no new click, nothing. The window only stays alive because each listener returns quickly.

1.2 Proof: who is running my code?

Add this throw-away panel to your td5 project as src/main/java/com/s7infa3/ThreadProbe.java and run it.

package com.s7infa3;

import javax.swing.*;

public class ThreadProbe {
    public static void main(String[] args) {
        System.out.println("main runs on: "
            + Thread.currentThread().getName());

        SwingUtilities.invokeLater(() -> {
            System.out.println("UI builder runs on: "
                + Thread.currentThread().getName());

            JButton b = new JButton("Click me");
            b.addActionListener(e ->
                System.out.println("listener runs on: "
                    + Thread.currentThread().getName()));

            JFrame f = new JFrame("Thread probe");
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            f.add(b);
            f.pack();
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        });
    }
}

Expected output (after one click):

main runs on: main
UI builder runs on: AWT-EventQueue-0
listener runs on: AWT-EventQueue-0

main builds the window request, but the UI and every listener run on AWT-EventQueue-0 — that is the EDT. Remember this name: when an exception stack trace mentions AWT-EventQueue-0, the bug is in your UI code.

Rule of thumb. Listeners must be short. If a listener needs to do slow work, it must not do it directly (Parts 3–4 show why, and how to fix it).


Part 2 — A Non-Blocking Animation with javax.swing.Timer (30 min)

Animation is the right way to do repeated work on the EDT: a javax.swing.Timer fires a short ActionListener at regular intervals. Each tick updates the model a little and calls repaint(). Because each tick is tiny, the EDT stays free between ticks and the window stays responsive.

Careful — two classes named Timer. Use javax.swing.Timer, not java.util.Timer. The Swing one fires its callback on the EDT, so it is safe to call repaint() from it. Add import javax.swing.Timer; explicitly to avoid the clash.

2.1 Goal

Add a pulsing highlight to the selected cell of your TD5 GridPanel: the light-blue selection fill should breathe — brighten and dim continuously — as long as a cell is selected.

2.2 Add an animation phase to GridPanel

Add a field that grows on every tick and a Timer that drives it. Put this in your GridPanel:

import javax.swing.Timer;   // add at the top — NOT java.util.Timer

// ... inside the class ...

private double phase = 0.0;          // animation phase, grows each tick
private Timer  animator;             // fires ~30 times per second

private void startAnimation() {
    animator = new Timer(33, e -> {  // 33 ms ≈ 30 frames per second
        phase += 0.15;               // advance the model a little
        repaint();                   // ask Swing to redraw
    });
    animator.start();
}

Call startAnimation() at the end of the GridPanel constructor.

2.3 Use the phase when painting the highlight

In paintComponent(), replace the fixed selection colour with one whose brightness depends on phase. A sine wave gives a smooth pulse between two blues:

if (selectedRow >= 0) {
    // pulse alpha between ~80 and ~180 using a sine wave
    int alpha = (int) (130 + 50 * Math.sin(phase));
    g2.setColor(new Color(120, 170, 255, alpha));  // 4th arg = transparency
    g2.fillRect(selectedCol * CELL, selectedRow * CELL, CELL, CELL);
}

Math.sin(phase) swings between −1 and +1, so alpha oscillates between 80 and 180 — the highlight fades in and out.

2.4 Expected behaviour

  • With no cell selected, nothing pulses (the highlight block is skipped).
  • Click a cell: its highlight now breathes smoothly, about one pulse per second.
  • The grid still reacts instantly to clicks — the animation never blocks input.

2.5 Variant — a moving dot (optional)

If you prefer motion to pulsing, animate a dot that slides across the panel. Keep an x field, advance it each tick, bounce at the edges, and draw it:

private int dotX = 0, dir = 4;

// in the Timer callback, instead of phase += ...:
dotX += dir;
if (dotX < 0 || dotX > getWidth() - 20) dir = -dir;   // bounce
repaint();

// in paintComponent(), after the grid:
g2.setColor(new Color(229, 57, 53));
g2.fillOval(dotX, getHeight() - 24, 16, 16);

Stopping the animation. A Timer keeps firing forever. To stop it (e.g. when the game ends), call animator.stop(). To pause and resume, use stop() / start().


Part 3 — Freezing the UI on Purpose (25 min)

Now we break it deliberately, so the failure is unforgettable.

3.1 A slow task

Imagine a button that runs an expensive computation — say, "think about the best move". We simulate the cost with a Thread.sleep loop that counts to N. This is the wrong way to do it.

Add a button and this listener to a small test frame (you can reuse ThreadProbe.java or add it to your TD5 App):

JButton workBtn = new JButton("Compute (BAD)");
JLabel  result  = new JLabel("idle");

workBtn.addActionListener(e -> {
    long sum = 0;
    for (int i = 1; i <= 50; i++) {
        sum += i;
        try { Thread.sleep(100); } catch (InterruptedException ex) {}
        // (we even try to show progress — it will NOT appear!)
        result.setText("working… " + i + "/50");
    }
    result.setText("done, sum = " + sum);
});

3.2 Run it and observe

Click Compute (BAD). The loop takes 5 seconds (50 × 100 ms). During those 5 seconds:

  • The button stays stuck pressed and grey.
  • The label does not count up — it jumps straight to done, sum = 1275 at the very end.
  • You cannot move, resize, or close the window — it is completely frozen.
  • On macOS you may see the spinning beach-ball; on Windows the title bar says "Not Responding".

3.3 Why? The EDT is blocked

Recall Part 1: the EDT processes events one at a time, in a loop. Your listener is one of those events — and it does not return for 5 seconds. While it runs:

EDT is busy inside your for-loop  ⟶  it cannot:
    - repaint the button or the label   (so result.setText is invisible)
    - handle the window's move/close events
    - process any new click

Every result.setText(...) you called did change the label's text, but the actual repaint is itself an event sitting in the queue — and the queue is not being drained, because the EDT is stuck inside your loop. Only when the listener finally returns does the EDT get to repaint, and by then the loop is over, so you only ever see the final value.

The one-sentence rule. Never block the EDT. Any task longer than a few tens of milliseconds must run on another thread.


Part 4 — The Fix: SwingWorker (35 min)

SwingWorker<T, V> is Swing's tool for exactly this: it runs the slow work on a background thread, while giving you safe hooks to update the UI on the EDT.

Method Runs on Purpose
doInBackground() background thread the slow work — no Swing calls here
publish(V…) background thread send intermediate values toward the UI
process(List<V>) EDT receive published values, update components
done() EDT called once when the work finishes — final UI update

The two type parameters: T is the final result type (returned by doInBackground, read with get() in done); V is the type of the progress values you publish.

4.1 Rewrite the slow button correctly

JButton goodBtn = new JButton("Compute (GOOD)");

goodBtn.addActionListener(e -> {
    goodBtn.setEnabled(false);            // avoid double-starting
    result.setText("working… 0/50");

    SwingWorker<Long, Integer> worker = new SwingWorker<>() {

        @Override
        protected Long doInBackground() throws Exception {
            long sum = 0;
            for (int i = 1; i <= 50; i++) {
                sum += i;
                Thread.sleep(100);        // the slow work — off the EDT
                publish(i);               // report progress: i in [1..50]
            }
            return sum;                   // final result (type T = Long)
        }

        @Override
        protected void process(java.util.List<Integer> chunks) {
            // runs on the EDT — safe to touch Swing here
            int latest = chunks.get(chunks.size() - 1);
            result.setText("working… " + latest + "/50");
        }

        @Override
        protected void done() {
            // runs on the EDT once doInBackground returns
            try {
                long sum = get();                 // the value we returned
                result.setText("done, sum = " + sum);
            } catch (Exception ex) {
                result.setText("failed: " + ex.getMessage());
            }
            goodBtn.setEnabled(true);
        }
    };

    worker.execute();   // start the background thread (returns immediately)
});

4.2 Expected behaviour

Click Compute (GOOD):

  • The button immediately greys out (disabled) but the window stays responsive — you can move it, resize it, click elsewhere.
  • The label counts up live: working… 1/50, working… 2/50, … one step every 100 ms.
  • After 5 seconds: done, sum = 1275, and the button re-enables.

Same computation, same 5 seconds — but now the EDT is free the whole time, so progress is visible and the UI never freezes.

4.3 Why this works

execute() launches doInBackground() on a worker thread and returns instantly, so the listener finishes and the EDT goes back to draining events (repaints, clicks). Each publish(i) hands a value to Swing, which calls process(...) on the EDT — that is why it is safe to call result.setText(...) there. When doInBackground returns, Swing calls done() on the EDT, where you read the result with get() and do the final update.

The golden rule. Slow work → doInBackground (background). UI updates → process / done (EDT). Crossing the wrong way (Swing calls in doInBackground, or sleeping in done) reintroduces the freeze or a race.

This is the pattern you will reuse in BE1. When the game has to compute a move (now, or an AI later), do it in a SwingWorker so the board keeps repainting and a status label can say "computing move…":

statusLabel.setText("computing move…");

new SwingWorker<int[], Void>() {
    @Override
    protected int[] doInBackground() {
        // pretend the move search is expensive
        try { Thread.sleep(1500); } catch (InterruptedException ex) {}
        return new int[] { 1, 2 };          // chosen (row, col)
    }
    @Override
    protected void done() {
        try {
            int[] move = get();
            board[move[0]][move[1]] = 2;     // apply the move to the model
            statusLabel.setText("your turn");
            repaint();                       // redraw the panel — on the EDT
        } catch (Exception ex) { /* handle */ }
    }
}.execute();

While the worker runs, your Part 2 Timer animation keeps pulsing — proof the UI is alive — and the click handlers still respond. That is the whole point.

4.5 Closing note — the same rule on Android (CM6)

This is not a Swing quirk. Every GUI toolkit has a single UI thread. On Android (Part 3 / CM6) it is called the main thread (or UI thread), and the rule is identical:

  • Touching a View from another thread is illegal.
  • Doing slow work (network, disk, computation) on the main thread triggers an "Application Not Responding" (ANR) dialog — Android's version of the frozen window you saw in Part 3.

The Java tools differ in name but not in spirit: where Swing uses SwingWorker + done(), Android uses background executors / coroutines and posts results back to the main thread (runOnUiThread, Handler). Learn the pattern here once — slow work off the UI thread, UI updates back on it — and you already know the Android rule.


Deliverable

No formal submission. Before leaving, make sure you can demonstrate all three behaviours on your TD5 grid:

  1. The selected cell pulses via a javax.swing.Timer (Part 2).
  2. The bad button visibly freezes the window (Part 3) — you must be able to explain why in one sentence.
  3. The good button runs the same work with a live progress label and a responsive window (Part 4).

Keep the SwingWorker "computing move…" snippet — you will reuse it in BE1.


Common Errors

Error Cause Fix
Animation never moves Used java.util.Timer instead of javax.swing.Timer Import javax.swing.Timer; its callback runs on the EDT
Window still freezes with a Timer Did slow work inside the Timer tick Each tick must be tiny; long work belongs in SwingWorker
Progress label jumps to the final value Slow loop runs directly in the ActionListener (EDT blocked) Move the loop into SwingWorker.doInBackground()
process / done never updates the UI Called repaint/setText inside doInBackground() Touch Swing only in process / done (they run on the EDT)
done() is empty / result lost Forgot get(), or didn't return from doInBackground Return the result; read it with get() inside done()
Button can be clicked again mid-run Forgot to disable it before execute() setEnabled(false) at start, re-enable in done()
repaint() does nothing during a long task The EDT is blocked, so the repaint request never runs Never block the EDT — offload to SwingWorker