Precursor: Event Distribution Stream (EDT) .
Swing single-threaded. What does it mean?
All Swing processing starts with an event. EDT is a thread that processes these events in a loop in the following directions (but more complex):
class EventDispatchThread extends Thread { Queue<AWTEvent> queue = ...; void postEvent(AWTEvent anEvent) { queue.add(anEvent); } @Override public void run() { while (true) { AWTEvent nextEvent = queue.poll(); if (nextEvent != null) { processEvent(nextEvent); } } } void processEvent(AWTEvent theEvent) {
The dispatch flow is hidden from us through abstraction: we usually only write listener callbacks.
- Pressing the button publishes the event ( in native code ): when the event is processed,
actionPerformed is called in actionPerformed . - The
repaint call publishes the event: when the event is processed, paintComponent is called in the EDT. invokeLater event message call: when the event is processed, run is called on EDT .- Everything in Swing starts with an event.
Event tasks are processed in sequence, in the order they are placed.
The following event can only be processed when the current event task is returned. That is why we cannot have an endless loop on EDT. actionPerformed (or run , as in your edit) never returns, so calls to repaint post paint events, but they are never handled, and the program seems to freeze.
This is what it means to βblockβ EDT.
There are two main ways to make animations in a Swing program:
Use Thread (or SwingWorker ).
The advantage of using a stream is that the processing is performed outside of the EDT, so during intensive processing, the graphical user interface can be updated at the same time.
Use javax.swing.Timer .
The advantage of using a timer is that the processing is done in the EDT, so there is no need to worry about synchronization and safely change the state of the GUI components.
Generally speaking, we should only use a thread in a Swing program if there is a reason not to use a timer.
For the user, there is no noticeable difference between them.
Your call to revalidate tells me that you are changing the state of components in a loop (add, remove, change location, etc.). It is not necessarily safe to do from EDT. If you change the state of components, this is a good reason to use a timer rather than a thread. Using a stream without proper synchronization can lead to minor errors that are difficult to diagnose. See Memory Consistency Errors .
In some cases, operations on a component are performed under a tree lock (Swing independently ensures that they are thread-oriented), but in some cases this is not so.
We can rotate the cycle of the following form:
while ( condition() ) { body(); Thread.sleep( time ); }
in Timer following form:
new Timer(( time ), new ActionListener() { @Override public void actionPerformed(ActionEvent evt) { if ( condition() ) { body(); } else { Timer self = (Timer) evt.getSource(); self.stop(); } } }).start();
Here is a simple example demonstrating animation with both a stream and a timer. The green bar cyclically moves around the black panel.

import javax.swing.*; import java.awt.*; import java.awt.event.*; class SwingAnimation implements Runnable{ public static void main(String[] args) { SwingUtilities.invokeLater(new SwingAnimation()); } JToggleButton play; AnimationPanel animation; @Override public void run() { JFrame frame = new JFrame("Simple Animation"); JPanel content = new JPanel(new BorderLayout()); play = new JToggleButton("Play"); content.add(play, BorderLayout.NORTH); animation = new AnimationPanel(500, 50); content.add(animation, BorderLayout.CENTER); // 'true' to use a Thread // 'false' to use a Timer if (false) { play.addActionListener(new ThreadAnimator()); } else { play.addActionListener(new TimerAnimator()); } frame.setContentPane(content); frame.pack(); frame.setLocationRelativeTo(null); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setVisible(true); } abstract class Animator implements ActionListener { final int period = ( 1000 / 60 ); @Override public void actionPerformed(ActionEvent ae) { if (play.isSelected()) { start(); } else { stop(); } } abstract void start(); abstract void stop(); void animate() { int workingPos = animation.barPosition; ++workingPos; if (workingPos >= animation.getWidth()) { workingPos = 0; } animation.barPosition = workingPos; animation.repaint(); } } class ThreadAnimator extends Animator { volatile boolean isRunning; Runnable loop = new Runnable() { @Override public void run() { try { while (isRunning) { animate(); Thread.sleep(period); } } catch (InterruptedException e) { throw new AssertionError(e); } } }; @Override void start() { isRunning = true; new Thread(loop).start(); } @Override void stop() { isRunning = false; } } class TimerAnimator extends Animator { Timer timer = new Timer(period, new ActionListener() { @Override public void actionPerformed(ActionEvent ae) { animate(); } }); @Override void start() { timer.start(); } @Override void stop() { timer.stop(); } } static class AnimationPanel extends JPanel { final int barWidth = 10; volatile int barPosition; AnimationPanel(int width, int height) { setPreferredSize(new Dimension(width, height)); setBackground(Color.BLACK); barPosition = ( width / 2 ) - ( barWidth / 2 ); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); int width = getWidth(); int height = getHeight(); int currentPos = barPosition; g.setColor(Color.GREEN); g.fillRect(currentPos, 0, barWidth, height); if ( (currentPos + barWidth) >= width ) { g.fillRect(currentPos - width, 0, barWidth, height); } } } }