Saltar al contenido principal

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 (doInBackground retorna este tipo).
  • V: tipo de los resultados parciales (para publish/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 un ActionListener → congela la interfaz.
  • Modificar un componente desde un Thread normal → bugs visuales.
  • Llamar a SwingUtilities.invokeAndWait desde el EDT → deadlock.
  • No comprobar isCancelled() en doInBackground → el worker no responde a la cancelación.