Concurrencia en Swing
Uno de los aspectos más críticos del desarrollo con Swing es entender cómo funciona su modelo de hilos (thread model).
El Event Dispatch Thread (EDT)
Swing no es thread-safe, es decir, no está diseñado para ser usado desde múltiples hilos.
Swing tiene un único hilo para procesar eventos y actualizar la interfaz: el Event Dispatch Thread (EDT).
EDT (Event Dispatch Thread)
│
├─ Procesa eventos (clics, teclas, redimensiones...)
├─ Ejecuta listeners (ActionListener, MouseListener...)
└─ Repinta componentes (paint, repaint)
Reglas fundamentales:
- Toda creación y modificación de GUI en el EDT →
SwingUtilities.invokeLater. - Nunca bloquees el EDT → Sin
Thread.sleep, sin E/S, sin operaciones lentas. - Nunca modifiques componentes desde otro hilo → Puede causar bugs visuales y condiciones de carrera.
SwingUtilities.invokeLater e invokeAndWait
// invokeLater: encola la tarea en el EDT y RETORNA INMEDIATAMENTE
SwingUtilities.invokeLater(() -> {
label.setText("Actualizado");
tabla.repaint();
});
// invokeAndWait: encola y ESPERA a que termine (bloquea el hilo actual)
// NUNCA llamar desde el EDT (deadlock)
try {
SwingUtilities.invokeAndWait(() -> {
label.setText("Actualizado");
});
} catch (InvocationTargetException | InterruptedException e) {
e.printStackTrace();
}
// Verificar si estás en el EDT
if (SwingUtilities.isEventDispatchThread()) {
// Estás en el EDT
} else {
SwingUtilities.invokeLater(() -> { /* código de GUI */ });
}
El problema: tareas largas en el EDT
Si ejecutas una tarea lenta en el EDT (red, base de datos, procesamiento), la interfaz se congela.
// ❌ MAL: bloquea la interfaz
btnDescargar.addActionListener(e -> {
byte[] datos = descargarArchivo(); // ← Puede tardar segundos
mostrarDatos(datos); // La interfaz se congela mientras tanto
});
// ✅ BIEN: ejecutar en hilo separado
btnDescargar.addActionListener(e -> {
new Thread(() -> {
byte[] datos = descargarArchivo(); // En hilo secundario
SwingUtilities.invokeLater(() -> {
mostrarDatos(datos); // De vuelta en el EDT
});
}).start();
});
SwingWorker — La solución correcta
SwingWorker<T, V> es la clase diseñada para ejecutar tareas largas en segundo plano y comunicar los resultados al EDT de forma segura.
T: tipo del resultado final (doInBackgroundretorna este tipo).V: tipo de los resultados parciales (parapublish/process).
Estructura básica
SwingWorker<String, Void> worker = new SwingWorker<>() {
@Override
protected String doInBackground() throws Exception {
// Se ejecuta en hilo secundario (NO en el EDT)
// Aquí va la lógica lenta: red, BD, archivo...
Thread.sleep(3000);
return "Resultado de la operación";
}
@Override
protected void done() {
// Se ejecuta en el EDT cuando doInBackground termina
try {
String resultado = get(); // Obtener el resultado (puede lanzar excepciones)
lblResultado.setText(resultado);
} catch (InterruptedException | ExecutionException e) {
lblResultado.setText("Error: " + e.getCause().getMessage());
}
btnEjecutar.setEnabled(true);
barraProgreso.setIndeterminate(false);
}
};
// Lanzar el worker
btnEjecutar.setEnabled(false);
barraProgreso.setIndeterminate(true);
worker.execute();
// Cancelar
worker.cancel(true); // true = interrumpir el hilo si está esperando
boolean cancelado = worker.isCancelled();
boolean terminado = worker.isDone();
Con progreso parcial (publish/process)
SwingWorker<Void, Integer> workerProgreso = new SwingWorker<>() {
@Override
protected Void doInBackground() throws Exception {
for (int i = 0; i <= 100; i++) {
if (isCancelled()) break; // Respetar la cancelación
procesarElemento(i); // Trabajo real
publish(i); // Enviar progreso parcial al EDT
setProgress(i); // Actualizar propiedad "progress" (0-100)
}
return null;
}
@Override
protected void process(java.util.List<Integer> chunks) {
// Se ejecuta en el EDT (puede llamarse varias veces agrupando valores)
int ultimo = chunks.get(chunks.size() - 1);
barraProgreso.setValue(ultimo);
lblEstado.setText("Procesando: " + ultimo + "%");
}
@Override
protected void done() {
if (isCancelled()) {
lblEstado.setText("Cancelado");
} else {
lblEstado.setText("¡Completado!");
barraProgreso.setValue(100);
}
}
};
// Escuchar cambios en la propiedad "progress"
workerProgreso.addPropertyChangeListener(e -> {
if ("progress".equals(e.getPropertyName())) {
int progreso = (Integer) e.getNewValue();
barraProgreso.setValue(progreso);
}
});
workerProgreso.execute();
Ejemplo completo: descarga con progreso y cancelación
public class DescargaWorker extends SwingWorker<byte[], Integer> {
private final String url;
private final JProgressBar barra;
private final JLabel estado;
private final JButton btnCancelar;
public DescargaWorker(String url, JProgressBar barra, JLabel estado, JButton btnCancelar) {
this.url = url;
this.barra = barra;
this.estado = estado;
this.btnCancelar = btnCancelar;
}
@Override
protected byte[] doInBackground() throws Exception {
java.net.URL conexion = new java.net.URL(url);
java.net.HttpURLConnection http = (java.net.HttpURLConnection) conexion.openConnection();
int tamano = http.getContentLength();
try (java.io.InputStream in = http.getInputStream();
java.io.ByteArrayOutputStream out = new java.io.ByteArrayOutputStream()) {
byte[] buffer = new byte[4096];
int leido;
int total = 0;
while ((leido = in.read(buffer)) != -1) {
if (isCancelled()) {
http.disconnect();
return null;
}
out.write(buffer, 0, leido);
total += leido;
if (tamano > 0) {
publish((int) (100.0 * total / tamano));
}
}
return out.toByteArray();
}
}
@Override
protected void process(java.util.List<Integer> chunks) {
int p = chunks.get(chunks.size() - 1);
barra.setValue(p);
estado.setText("Descargando... " + p + "%");
}
@Override
protected void done() {
btnCancelar.setEnabled(false);
try {
if (isCancelled()) {
estado.setText("Descarga cancelada");
barra.setValue(0);
} else {
byte[] datos = get();
estado.setText("Descarga completa: " + datos.length + " bytes");
barra.setValue(100);
}
} catch (Exception e) {
estado.setText("Error: " + e.getMessage());
}
}
}
// Uso:
DescargaWorker worker = new DescargaWorker(url, barraProgreso, lblEstado, btnCancelar);
btnCancelar.addActionListener(e -> worker.cancel(true));
worker.execute();
javax.swing.Timer — Tareas periódicas en el EDT
A diferencia de java.util.Timer, javax.swing.Timer ejecuta en el EDT, lo que hace seguro modificar componentes.
// Reloj en tiempo real
JLabel reloj = new JLabel();
Timer timerReloj = new Timer(1000, e -> {
reloj.setText(new java.text.SimpleDateFormat("HH:mm:ss").format(new java.util.Date()));
});
timerReloj.start();
// Animación (60 fps ≈ cada 16ms)
final int[] x = {0};
Timer animacion = new Timer(16, e -> {
x[0] = (x[0] + 2) % getWidth();
canvas.repaint();
});
animacion.start();
// Tarea única con retardo
Timer retardo = new Timer(2000, e -> {
JOptionPane.showMessageDialog(null, "Han pasado 2 segundos");
});
retardo.setRepeats(false);
retardo.start();
Patrón completo: interfaz responsiva
public class AppResponsiva extends JFrame {
private JButton btnProcesar;
private JButton btnCancelar;
private JProgressBar barra;
private JTextArea log;
private SwingWorker<Void, String> worker;
public AppResponsiva() {
super("App Responsiva");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(500, 350);
btnProcesar = new JButton("Iniciar proceso");
btnCancelar = new JButton("Cancelar");
btnCancelar.setEnabled(false);
barra = new JProgressBar(0, 100);
barra.setStringPainted(true);
log = new JTextArea();
log.setEditable(false);
btnProcesar.addActionListener(e -> iniciarProceso());
btnCancelar.addActionListener(e -> {
if (worker != null) worker.cancel(true);
});
JPanel controles = new JPanel(new FlowLayout());
controles.add(btnProcesar);
controles.add(btnCancelar);
setLayout(new BorderLayout(5, 5));
add(controles, BorderLayout.NORTH);
add(barra, BorderLayout.SOUTH);
add(new JScrollPane(log), BorderLayout.CENTER);
setLocationRelativeTo(null);
}
private void iniciarProceso() {
btnProcesar.setEnabled(false);
btnCancelar.setEnabled(true);
barra.setValue(0);
log.setText("");
worker = new SwingWorker<>() {
@Override
protected Void doInBackground() throws Exception {
for (int i = 1; i <= 10; i++) {
if (isCancelled()) break;
Thread.sleep(500); // Simular trabajo
publish("Paso " + i + " completado");
setProgress(i * 10);
}
return null;
}
@Override
protected void process(java.util.List<String> mensajes) {
mensajes.forEach(m -> log.append(m + "\n"));
}
@Override
protected void done() {
btnProcesar.setEnabled(true);
btnCancelar.setEnabled(false);
if (isCancelled()) {
log.append("--- Proceso cancelado ---\n");
} else {
log.append("=== Proceso completado ===\n");
barra.setValue(100);
}
}
};
worker.addPropertyChangeListener(e -> {
if ("progress".equals(e.getPropertyName())) {
barra.setValue((Integer) e.getNewValue());
}
});
worker.execute();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> new AppResponsiva().setVisible(true));
}
}
Errores más comunes
Thread.sleep()en unActionListener→ congela la interfaz.- Modificar un componente desde un
Threadnormal → bugs visuales. - Llamar a
SwingUtilities.invokeAndWaitdesde el EDT → deadlock. - No comprobar
isCancelled()endoInBackground→ el worker no responde a la cancelación.