TD-Threads — Concurrency: the EDT & SwingWorker
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 withpublish/processand finishing indone() - 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:
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 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. Usejavax.swing.Timer, notjava.util.Timer. The Swing one fires its callback on the EDT, so it is safe to callrepaint()from it. Addimport 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
Timerkeeps firing forever. To stop it (e.g. when the game ends), callanimator.stop(). To pause and resume, usestop()/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 = 1275at 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 indoInBackground, or sleeping indone) reintroduces the freeze or a race.
4.4 Apply it to a panel — "computing move…" (optional but recommended)
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
Viewfrom 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:
- The selected cell pulses via a
javax.swing.Timer(Part 2). - The bad button visibly freezes the window (Part 3) — you must be able to explain why in one sentence.
- 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 |