Swing no es reentrante

From ChuWiki
Jump to navigation Jump to search

El problema de SWING con varios hilos[edit]

La documentación de java advierte que SWING no es "thread safe", es decir, no se puede trabajar simultáneamente con varios hilos con swing de una forma segura.

¿Cual es el problema?

Imaginemos que tenemos nuestras ventanas, en concreto un JList que está mostrando un DefaultListModel. Imaginemos también, por ejemplo, que tenemos un hilo independiente atendiendo un socket. Por ese socket nos llegan datos que deben añadirse, borrarse o modificarse en el DefaultListModel.

Cuando se necesite refrescar el JList, porque el usuario esté seleccionando items, jugando con el scroll o lo que sea, el hilo de awt realiza un código similar al siguiente

int numeroElementos = defaultListModel.getRowCount();
for (int i=0;i<numeroElementos;i++)
   pintaElemento (i);

Supongamos que hay 10 elementos y el bucle se está ejecutando, estamos repintando el elemento 5. En ese momento llega un mensaje por el socket que nos indica que borremos de la lista uno de los elementos, por ejemplo, el 3. El hilo de awt sigue con su bucle hasta 10, con lo que cuando llegue al elemento 10 (de indice 9) dará una excepción puesto que ya sólo quedan 9 elementos (de indices 0 a 8).

Da igual que pongamos synchronized en el modelo, ya que se están realizando varios accesos.


La solución[edit]

La solución que propone la documentación de java es usar SwingUtilites.invokeLater() para cualquier código que afecte al pintado de ventanas. En el caso del ejemplo, para borrar el elemento desde el hilo del socket, debemos hacerlo así

SwingUtilities.invokeLater (new Runnable()
{
   public void run()
   {
      modelo.removeElement(posicion);
   }
});

Esto hará que el código de dentro del método run se encole y lo ejecute más adelante el mismo hilo de awt. Si el hilo de awt está ocupado refrescando el JList, hasta que no termine no borrará el elemento.


Mi solución[edit]

A mí este método propuesto por la documentación de java me parece engorroso por varios motivos:

  • Hay que acordarse de hacerlo siempre que se quiere tocar el modelo de datos.
  • Si haces clases reutilizables para mucha gente, hay que advertir claramente que se debe hacer así y no tienes garantía de que todo el mundo recuerde que hay que hacerlo.

La solución que yo uso y que sólo hay que hacer una vez consiste en:

Me hago mi propio modelo de datos, en el que guardo los datos por duplicado. Realmente los datos no se duplican, sólo las listas que los mantienen.

public class MiModelo
{
   private LinkeList listaDatos; // Mi lista de datos
   private DefaultListModel modeloLista // Para el JList
}

Por supuesto añado los métodos necesarios para añadir, borrar y eliminar elementos, pero lo añado al DefaultListModel con un SwingUtilities.invokeLater()

public void addElement (Object elemento)
{
   listaDatos.addElement (elemento);
   SwingUtilities.invokeLater (new Runnable()
   {
      public void run()
      {
          modeloLista.add(elemento);
      }
   });
}

Luego, para meter en el JList, pongo un metodo getModeloLista que me devuelve el DefaultListModel

MiModelo modelo = new MiModelo();
JList lista = new JList (modelo.getModeloLista());

Con esto, puedo usar mi modelo sin necesidad de recordar los SwingUtilities.invokeLater(). El JList se refrescará con un poco de retraso, pero lo iba a hacer de todas formas con la otra solución.

Los método aquí puesto para LinkedList y DefaultListModel son de memoria, es posible que no estén bien, pero espero que la idea se entienda.

Resumiendo, mantengo dos modelos de datos. Uno para trabajar y el otro exclusivamente para el JList.

Enlaces[edit]

  • En perfecto inglés, un capítulo de muestra de un libro : Swing Threading