Herramientas de usuario

Herramientas del sitio


bloque1:hilos

Programación Concurrente

Concurrencia

La concurrencia es la ejecución de diferentes tareas por la CPU de un ordenador, compartiendo los recursos del sistema entre ellas (memoria, ficheros, periféricos, etc). Cuando hablamos de concurrencia podemos hablar de procesos o de hilos.

Procesos

Un programa se puede definir como un conjunto de instrucciones que persiguen una finalidad, diseñados es un lenguaje de programación. Un proceso es un programa en estado de ejecución. Consiste en el código del programa, los datos, un contador de programa, etc.

Multiproceso

Es la capacidad de ejecutar varios procesos de forma simultánea. Los Sistemas Operativos actuales disponen de mecanismos que permiten la ejecución de muchos procesos. A estos SSOO les llamamos multitarea.

Para que los distintos procesos se puedan ejecutar concurrentemente, el sistema operativo debe parar e iniciar los procesos para irles asignando tiempo de ejecución. Cuando se para un proceso para ejecutar otro, se debe guardar el estado del proceso para reanudar su ejecución posteriormente. Cada vez que el sistema detiene un proceso para ejecutar otro, debe guardar su información es una estructura de datos llamado Bloque de Control de Proceso. A este proceso se le conoce como cambio de contexto:

  • Guardar el estado del programa que se estaba ejecutando en SU BCP.
  • Seleccionar otro programa para ejecutar mediante un algoritmo de planificación.
  • Restaurar el estado del programa seleccionado a partir de su BCP.
  • Ejecutar el programa seleccionado.
Figure 1: Ciclo de vida de los procesos

Multihilo

Los hilos permiten ejecutar diferentes tareas dentro de un mismo proceso, mejorando el rendimiento de la CPU. En este caso una misma aplicación puede ejecutar diferentes hilos de forma concurrente que se encargan de diferentes tareas. Todos estos hilos comparten recursos del proceso.

Algoritmos de planificación

En entornos concurrentes, un algoritmo de planificación se encarga de elegir qué proceso o hilo se ejecutará a continuación. Un algoritmo de planificació debe:

  • Ser imparcial y eficiente
  • Minimizar el tiempo de respuesta al usuario, sobre todo en aquellas tareas más interactivas
  • Ejecutar el mayor número de procesos en el menor tiempo posible
  • Mantener un equilibrio en el uso de los recursos del sistema

FCFS: First Come First Served

El primer proceso que llegue al procesador se ejecuta antes y de forma completa. Hasta que su ejecución no termina no podrá pasarse a ejecutar otro proceso.

RR: Round Robin

Se le conoce también como algoritmo de turno rotatorio. En este caso se designa una cantidad corta de tiempo (quantum) de procesamiento a todas las tareas. Las que necesiten más tiempo de proceso deberán esperar a que vuelva a ser su turno para seguir ejecutándose.

SPF: Shortest Process First

En este algoritmo, de todos los procesos listos para ser ejecutados, lo hará primero el más corto

SRT: Shortest Remaining Time

De todos los procesos listos para ejecución, se ejecutará aquel al que le quede menos tiempo para terminar.

Varias colas con realimentación

Es un algoritmo más complejo que todos los anteriores y, por tanto, más realista. Se utiliza en entornos donde se desconoce el tiempo de ejecución de un proceso al inicio de su ejecución. En este caso, el sistema dispone de varias colas que a su vez pueden disponer de diferentes políticas unas de otras. Los procesos van pasando de una cola a otra hasta que terminan su ejecución. En algunos casos, el algoritmo puede adaptarse modificando el número de colas, su política, . . .

multinivel.jpg
Figure 2: Algoritmo varias colas con realimentación

Ejercicios

  1. Escribe un programa en Java que atienda una serie de tareas. Cada tarea será representada mediante un objeto con un nombre y un entero que indica el tiempo que durará la tarea. El programa nos permitirá dar de alta una serie de tareas dando su nombre y su tiempo, y posteriormente ejecutarlas todas seleccionándolas tal y como hacen los siguientes algoritmos. Muestra las tareas que se van realizando en cada momento y el tiempo en que se inicia y termina cada una:
    1. Usando un algoritmo FCFS.
    2. Usando un algoritmo RR
    3. Usando un algoritmo SRT

Programación concurrente, paralela y distribuida

Programación concurrente

Es la programación de aplicaciones capaces de realizar varias tareas de forma simultánea utilizando hilos o threads. En este caso todas las tareas compiten por el uso del procesador (lo más habitual es disponer sólo de uno) y en un instante determinado sólo una de ellas se encuentra en ejecución. Además, habrá que tener en cuenta que diferentes hilos pueden compartir información entre sí y eso complica mucho su programación y coordinación.

Programación paralela

Es la programación de aplicaciones que ejecutan tareas de forma paralela, de forma que no compiten por el procesador puesto que cada una de ellas se ejecuta en uno diferente. Normalmente buscan resultados comunes dividiendo el problema en varias tareas que se ejecutan al mismo tiempo.

Programación distribuida

Es la programación de aplicaciones en las que las tareas a ejecutar se reparten entre varios equipos diferentes (conectados en red, a los que llamaremos nodos). Juntos, estos equipos, forman lo que se conoce como un Sistema Distribuido, que busca formar redes de equipos que trabajen con un fin común 1)

concurrencia.jpg
Figure 3: Programación concurrente / paralela
distribuida.jpg
Figure 4: Programación distribuida

Programación multihilo en Java

A continuación, tenemos un esquema del desarrollo del bloque, a través de las diferentes etapas hasta la práctica final.

¿Qué es un hilo?

Los hilos o Threads son procesos ligeros que se ejecutan simultáneamente en un mismo proceso. La ejecución simultanea de diferentes hilos mejora el rendimiento y la utilización de la CPU ya que podemos realizar varias tareas al mismo tiempo. Todo programa usa como mínimo un hilo de ejecución que se conoce como hilo principal. Si no creamos explicitamente ningún otro hilo, dicho programa solo podrá ejecutar tareas secuencialmente.

Figure 5: Proceso VS Hilo

Cada lenguaje tiene su propia API para trabajar con hilos; en Java podemos crear hilos implementando la interface Runnable o heredando de la clase Thread.

En un entorno multihilo, el planificador de hilos de Java, que forma parte de la JVM, se encarga de asignar un periodo de tiempo de ejecución a cada hilo de modo que todos puedan ejecutarse concurrentemente.

Primer plano

Aquí se ejecuta únicamente un hilo llamado “hilo principal”. En este hilo hemos programado siempre, sin conocimiento de que estábamos trabajando ya con hilos. También, puede ser usado para hacer cálculos u otros procesamientos complejos, aunque estos deberían de evitarse en este hilo. Puedo comprobar si estoy en el hilo principal:

Thread.currentThread().getName();

Segundo plano

En inglés background, es donde se ejecutan el resto de hilos. El segundo plano tiene la característica de darse en el mismo momento que el primer plano. Estos hilos deben llevar las ejecuciones pesadas de la aplicación. El segundo plano es la ejecución que el usuario no ve, es más, ni le interesa; para el usuario no existe.

Implementación

Para la creación de hilos en Java disponemos de varias vías, combinando el uso de la clase y el interface según nos interese:

  • Heredando de la clase Thread
  • Implementando la interface Runnable
  • Utilizando una clase Anónima
  • Otras formas: expresión lambda, ó clase interna

Heredando/extendiendo la clase Thread

Debemos crear una clase que extienda de Thread y acordarnos de sobrescribir el método run(). Es la forma más sencilla pero tiene la limitación de que en Java, cada clase solo puede extender de una única clase.

Para iniciar un hilo debemos crear un objeto de nuestra nueva clase y llamar a su método start() que hereda de la clase Thread.

public class Hilo extends Thread {
  private int id;
 
  public Hilo(int id){
     this.id = id;
  }
 
  @Override
  public void run() {
      System.out.println("Soy el hilo numero " + id);
  }
}
 
. . .
public class HiloPrincipal {
  public static void main(String args[]) {
    Hilo hilo = null;
    for( int i = 0; i < 3; i++){
       hilo = new Hilo(i);
       hilo.start();
    }
    System.out.println("Fin del hilo principal");
  }
}
Iniciar y parar hilo: Extendiendo de Thread

Implementando la interface Runnable

Nos permite seguir extendiendo de otra clase así como implementar otras interfaces. Además, el compilador nos fuerza a sobrescribir el método run().

En este caso debemos crear un objeto Thread por cada hilo que necesitemos y pasarle nuestro objeto Runnable. Esto nos permite compartir el mismo objeto Runnable para cada hilo, muy útil cuando necesitamos que diferentes hilos compartan elementos del mismo objeto Runnable.

public class Tarea implements Runnable {
  private int id;
 
  public Hilo(int id){
     this.id = id;
  }
 
  @Override
  public void run() {
      System.out.println("Soy el hilo numero " + id);
  }
}
 
. . .
public class Programa {
  public static void main(String args[]) {
    Tarea tarea1 = new Tarea(1);
    Thread hilo1 = new Thread(tarea1);
 
    System.out.println("Inicio del Hilo principal");
    hilo1.start();
    System.out.println("Fin del hilo principal");
  }
}
Iniciar y parar hilo: Implementando Runnable

Mediante una clase anónima

En algunas situaciones puede resultar más cómodo crear un hilo utilizando una clase anónima. Esto se recomienda cuando la clase no tiene una estructura muy compleja.

La ventaja es que, desde el método run() se puede acceder directamente a miembros de la clase principal, sin necesidad de recibir parámetros.

public class Principal {
  public static void main(String args[]) {
    Thread hilo = new Thread(new Runnable() {
      @Override
      public void run() {
        System.out.println("Hilo secundario");
      }
    });
 
    hilo.start();
  }
}
Crear hilo mediante clase anónima

Otras formas

También se puede utilizar las expresiones lambda, o mediante una clase interna (definición de una clase dentro de otra):

//Usando lambdas (a partir de Java 8): similar a clase anónima
Thread hilo = new Thread(() -> { 
                                 //Contenido del método run()
                                 System.out.println("Hilo secundario"); 
                               });
hilo.start();
 
//Usando una clase interna
public class Programa{
   private int contador;
   private String cadena;
   . . .
 
   class Hilo implements Runnable{
      @Override 
      public void run(){
        System.out.println("soy un hilo y accedo al campo cadena: " + cadena);
      }
   }
}

Consideraciones al crear hilos

  • Si heredamos de la clase Thread cada hilo crea un objeto único asociado. Cuando implementamos Runnable podemos reutilizar el mismo objeto Runnable con diferentes hilos.
  • Siempre se debe sobreescribir (Override) el método run() e implementar allí lo que tiene que hacer el hilo.
  • Es habitual hilos cuyo método run() se ejecuta repetidamente mediante un bucle hasta que decidamos pararlo de agún modo. En estos casos es recomendable usar sleep() para no sobrecargar la CPU.
  • Los problemas vienen cuando existen varios hilos. Hay que tener en cuenta que pueden compartir datos y código y encontrarse en diferentes estados de ejecución.
  • La ejecución de nuestra aplicación será thread-safe si se puede garantizar una correcta manipulación de los datos que comparten los hilos de la aplicación sin resultados inesperados.
  • Además, en el caso de aplicaciones multihilo, también nos puede interesar sincronizar y comunicar unos hilos con otros.

Ejercicios

  1. ¿Qué pasa si ejecutas varias veces el código de los ejemplos? ¿Siempre ocurre lo mismo?
  2. ¿Existe alguna manera de asegurar que todo se va a ejecutar en un orden concreto?

Clase Thread

Contiene una serie de constructores y métodos para manejar hilos. Hay varios métodos que están obsoletos y que no debemos utilizar.

ConstructorFinalidad
Thread(Runnable objetoRunnable)Crea un hilo a partir del objeto Runnable
Thread(String name)Crea un hilo y le da valor a su propiedad name
Thread(Runnable objetoRunnable, String name)Crea un hilo a partir del objeto Runnable y da nombre al hilo
MétodoFinalidad
void start()Inicia el hilo. La JVM llama al método run()
boolean isAlive()Comprueba si se ha terminado el método run()
int getId()Devuelve el id del hilo
String getName()Devuelve el nombre del hilo
void setName(String name) Asigna un nombre al hilo
void join()Se espera la terminación del hilo que invoca a este método antes de continuar
static void sleep(long ms)Hace que el hilo espere la cantidad de ms
static void yield()Detiene temporalmente el hilo y permite que se ejecute otro
static Thread currentThread() Indica el hilo actual en ejecución
void interrupt() Desbloquea un hilo bloqueado (wait(),sleep(), join(), y activa el flag interupt status
static boolean isInterrupted()Comprueba el estado del flag interrupt status, y lo resetea

Ciclo de vida de un hilo

En Java, cada hilo de jecución pasa por diferentes estados como vemos en la siguiente imagen:

Figure 6: Estados de un hilo

La clase Thread tiene el método getState() el cual devuelve el valor del estado actual del hilo:

  • NEW – hilo que ha sido creado pero aun no ha sido iniciado (start())
  • RUNNABLE – hilo en ejecución, o preparado para ser ejecutado. Ha iniciado su método run()
  • TERMINATED – hilo que ha terminado su método run() o debido a alguna excepción no capturada.
  • BLOCKED, WAITING, TIMED_WAITING – hilo esperando a entrar a un bloque synchronized, o por llamadas a sleep(), wait(), join()

Programación Thread-Safe

Hace referencia al enfoque que debemos tomar a la hora de programar aplicaciones en las que diferentes hilos comparten el mismo proceso y no genera situaciones inesperadas.

  • Debe garantizar una correcta manipulación de los datos que comparten los hilos de la aplicación sin resultados inesperados.
  • Plantea una exclusión mutua en el acceso a bloques compartidos.
  • Los datos compartidos son accedidos de forma atómica, por solo un hilo al mismo tiempo.
  • Sincroniza el órden de acceso a los datos compartidos para evitar condiciones de carrera.

Sincronización de hilos

El API de Java proporciona una serie de métodos en la clase Thread para la sincronización de los hilos en una aplicación:

  • Thread.sleep(int) El hilo que ejecuta esta llamada permanece dormido durante el tiempo especificado como parámetro (en ms)
  • join() Se espera la terminación del hilo que invoca a este método antes de continuar
  • isAlive() Comprueba si el hilo permanece activo todavía (no ha terminado su ejecución)
  • yield() Sugiere al scheduler que sea otro hilo el que se ejecute (no se asegura)

Método Thread.sleep()

En este caso el hilo duerme (detiene su ejecución) durante el tiempo especificado (en ms). Durante ese periodo podrán ejecutarse otros hilos. Propaga una excepción de clase InterruptedException que debo controlar:

public class Tarea implements Runnable {
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println("Soy un hilo y esto es lo que hago");
      // Controlar la excepción
      Thread.sleep(500);
    }
  }
}

Método join()

El método join() hace que el hilo desde el que se invoca al método espera a la finalización del hilo sobre el que se invoca. Propaga una excepción de clase InterruptedException que debo controlar.

En este ejemplo, el hilo principal inicia dos hilos e invoca al método join() sobre los dos: espera a que ambos hilos se hayan ejecutado para continuar (o para lo que sea). Los hilos 1 y 2 se ejecutan simultáneamente.

public static void main(String args[]) {
  Hilo hilo1 = new Thread(new Tarea());
  Hilo hilo2 = new Thread(new Tarea());
 
  hilo1.start();
  hilo2.start();
 
  . . .
  . . .
  hilo1.join();
  hilo2.join();
 
  System.out.println("Fin de la ejecución del hilo principal y de los dos hilos");
}

En el siguiente caso, el hilo principal (hilo main) espera a que termine el hilo1 para iniciar el hilo2, y luego espera a que hilo2 termine: los hilos 1 y 2 se ejecutan uno después de otro.

public static void main(String args[]) {
  Hilo hilo1 = new Thread(new Tarea());
  Hilo hilo2 = new Thread(new Tarea());
 
  hilo1.start();
  hilo1.join();
 
  hilo2.start();
  hilo2.join();
 
  System.out.println("Fin de la ejecución del hilo principal y de los dos hilos");
}
Sincronización de hilos desde el hilo main
Sincronización interna entre hilos, sin depender del hilo main

Ejercicios

  1. ¿Qué pasa si eliminamos las llamadas al método join() en ambos casos?
  2. Prueba a hacerlo con más hilos. ¿Y con n hilos?

Método isAlive()

isAlive() está indicando que el hilo está vivo (ha iniciado su ejecución y aún no ha muerto, puede estar en cualquier estado intermedio, incluso durmiendo). Nos permite mantener la ejecución de un hilo mientras otro siga en ejecución:

public class TareaPrincipal implements Runnable {
  @Override
  public void run() {
    for (int i = 0; i < 10; i++) {
      System.out.println("Soy la TarePrincipal");
      try {
        Thread.sleep(500);
      } catch (InterruptedException ie) {
        ie.printStackTrace();
      }
    }
  }
}
 
. . .
public class TareaAlive implements Runnable {
  private Thread otroHilo;
 
  public TareaAlive(Thread otroHilo) {
    this.otroHilo = otroHilo;
  }
 
  @Override
  public void run() {
    while (otroHilo.isAlive()) {
      System.out.println("Yo hago cosas mientras el otro hilo siga en ejecución");
      try {
        Thread.sleep(500);
      } catch (InterruptedException ie) {
        ie.printStackTrace();
      }
    }
    System.out.println("El otro hilo ha terminado. Yo también");
  }
}
 
. . .
public class Programa {
  public static void main(String args[]) {
    TarePrincipal tareaPrincipal = new TareaPrincipal();
    Thread hiloPrincipal = new Thread(tareaPrincipal);
 
    TareaAlive tareaAlive = new TareaAlive(hiloPrincipal);
    Thread hiloAlive = new Thread(tareaAlive);
 
    hiloPrincipal.start();
    hiloAlive.start();
 
    System.out.println("Se han terminado los dos hilos?");
  }
}
Mantener la ejecución de un hilo dependiendo del estado de otro hilo

Ejercicios

  1. ¿Podemos asegurar el resultado de la aplicación anterior?
  2. ¿Y si lo hacemos extendiendo de la clase Thread a la hora de implementar los hilos?

Detener hilos

Dado que ciertos métodos de la clase Thread para alterar la ejecución de un hilo están obsoletos por no ser seguros, Java nos anima a que seamos nosotros quienes implementemos formas limpias de detener, o pausar y reanudar nuestros hilos.

La forma más sencilla de controlar la ejecución de un hilo que realiza tareas iterativas es utilizando una variable de control, la cual modifico cuando quiera parar el hilo:

private boolean ejecucion;
 . . .
public void parar(){
   this.ejecucion = false;
}
 
@Override
public void run() {
   while(ejecucion){
      //Realizo las tareas
   }
}
 
// En caso de usar un bucle for
@Override
public void run() {
   for(int i = 0; i < cantidad; i++)
      //Realizo las tareas
      . . .
      if(!ejecucion){
         break;
      }
   }
}

También podría pausar un hilo de forma similar a la anterior. Hay que tener en cuenta que si paro el hilo también debo parar la pausa.

@Override
public void run() {
   while(ejecucion){
      //Realizo las tareas
      . . .
      // Si activo la pausa permanezco en el bucle
      while(pausado){
         //Evito que la comprobación de pausa acapare la cpu
         Thread.sleep(200);
         //Compruebo si se detiene el hilo mientras está pausado
         if(!ejecucion){
            pausado = false;
            return;
         }   
      }
   }
}

Si las condiciones de parada o de pausa son comunes para varios hilos, debo optar por el uso de una variable atómica de control para asegurar un sistema Thread-safe.


Variables atómicas

En el package java.util.concurrent.atomic tenemos varias clases que nos permiten crear objetos Thread-Safe. Algunas de estas clases son AtomicBoolean o AtomicInteger. Usar este tipo de datos permite acceder y modificarlos de por diferentes hilos de forma segura.

Podemos acceder o modificar sus valores de forma segura mediante sus métodos set() y get().

AtomicBoolean ejecucion = new AtomicBoolean();
 
ejecucion.set(true);
 
while(ejecucion.get()){
 . . .
}

Método interrupt()

Si un hilo está en una situación de bloqueo porque ha ejecutado wait(), sleep(), join(), etc, no podrá comprobar el estado de una variable de control ya que está bloqueado. Si el bloqueo dura un largo tiempo, debo utilizar el método interrupt().

El método interrupt() no detiene un hilo, sino que solicita al hilo que se detenga en el primer momento que pueda. Es el programador quien debe gestionar esa solicitud.

interrupt() funciona del siguiente modo:

  • Si el hilo sobre el que se ejecuta está en un estado de bloque (wait, join o sleep), rompe ese bloqueo produciendo una excepción InterruptedException y continuando su ejecución.
  • Si el hilo no está bloqueado, la llamada a interrupt() no afecta a la ejecución pero modifica el estado de una variable de control (interrupt status) a true.
  • Para los casos en los que el hilo no está bloqueado, puedo comprobar el estado de interruption mediante el método Thread.interrupted():
@Override
public void run() {
   for (int i = 0; i < cantidad; i++) {
      //Operaciones
      . . .
      if (Thread.interrupted()) {
        // Se ha interrumpido, terminamos ejecución
        return;
      }
   }
}

Utilizando las dos formas vistas anteriormente para parar la ejecución de un hilo podemos plantear una solución del siguiente modo:

public class HiloTerminable implements Runnable {
    private Thread hilo;
    private AtomicBoolean ejecucion;
 
    public HiloTerminable () {
        ejecucion = new AtomicBoolean(false);
    }
 
    public void start() {
        hilo = new Thread(this);
        hilo.start();
    }
 
    public void stop() {
        ejecucion.set(false);
        hilo.interrupt();
    }
 
    public void run() {
        ejecucion.set(true);
        while (ejecucion.get()) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            	Thread.currentThread().interrupt();
                System.out.println("El hilo fue terminado por el usuario");
	    }
            // Operaciones del hilo
            . . .
        }
        ejecucion.set(false);
    }
}

Bloques y métodos Synchronized

Un mecanismo de exclusión mutua, mutex o monitor evita que una conjunto de operaciones puedan ser ejecutadas por más de un hilo al mismo tiempo. Protege el acceso a una sección de código (sección crítica) de un acceso concurrente de diversos hilos. La palabra synchronized nos permite crear un monitor:

  • Sólo permitirá ejecutar un bloque o método synchronized por un hilo a la vez. Por tanto, si existen varios bloques/métodos synchronized dentro de un objeto, sólo uno de ellos podrá ejecutarse al mismo tiempo por diversos hilos.
  • El resto de hilos que también necesitan acceder a los bloques/metodos synchronized permanecerán bloqueados.
  • Cuando el hilo finaliza la ejecución de un bloque/método synchronized todos los hilos que estaban en espera para obtenerlo, se reactivarán y un hilo al azar accederá a la sección.
public class Sincrona{
  private Object monitor;
  . . . 
  //Solo un hilo puede ejecutar el siguiente método al mismo tiempo
  //El resto de hilos permanecen bloqueados hasta que termina su ejecución 
  public synchronized void accesoExclusivo(){
     // Operaciones 
  }
 
  . . .
  //Lo mismo ocurre con este bloque
  synchronized(monitor){
    // Operaciones
  }
}

Ejercicios

  1. ¿Teniendo en cuenta el concepto de exclusión mutua que aplica synchronized, cómo podríamos contar del 1 al 10 entre dos hilos, de modo que cada hilo cuenta una unidad alternativamente?

Métodos wait() y notify()

Al utilizar un monitor (synchronized), Java nos permite utilizar algunos métodos pertenecientes a la clase Object para sincronizar hilos:

  • wait(): El hilo que invoca al método para su ejecución hasta que otro hilo llame a notify o notifyAll.
  • notify(): Continua la ejecución del hilo que invocó wait() y desbloquea un hilo al azar para acceder al monitor.
  • notifyAll(): Continua la ejecución del hilo que invocó wait() y desbloquea todos los hilos que quieren acceder al monitor.
private boolean pausado;
. . .
@Override
public void run() {
   for(int i = 0; i < cantidad; i++){
      if(pausado){
         esperar();
      }
      //operaciones
   }
}
 
public synchronized void esperar(){
   try {
      wait();
   } catch (InterruptedException e) {
      System.out.println("Hilo parado por el usuario");
   }
}
 
public void pausar(){
   pausado = true;
}
 
public synchronized void continuar(){
   pausado = false;
   notifyAll();
}
 
. . .
public static void main(String[] args){
   Thread hiloPausado = new Thread();
   hiloPausado.start();
 
   //Suspendo su ejecución
   hiloPausado.pausar();
   //Reanudo la ejecución
   hiloPausado.continuar();
}

En el ejemplo anterior vemos que podemos utilizar estos métodos para detener y reanudar la ejecución de un hilo.


Colecciones Thread-safe

La clase Collections nos permite crear estructuras de datos seguras ante lecturas y modificaciones concurrentes a partir de una estructura de datos cualquiera:

ArrayList<Persona> lista = new ArrayList<>();
List<Persona> listaSincronizada = Collections.synchronizedList(lista);
 
//Ahora los accesos y modificaciones son thread-safe
listaSincronizada.add(persona1);
listaSincronizada.get(2);
listaSincronizada.remove(persona1);

Sin embargo, si lo que queremos es recorrer la colección, es el programador el que debe asegurar un acceso seguro a dicha iteracion:

//Indicamos ''synchronized'' para asegurar el acceso de un solo hilo
public synchronized void recorrerLista(){
   for(Persona persona : listaSincronizada){
      persona.hacerOperacion();
   }
}

Uso de hilos en GUIs Swing

Como se detalla en la documentación de Java, Concurrencia en Swing, una aplicación con GUI es ejecutada por Java usando diferentes hilos, concretamente 3 tipos de hilos:

  • Un hilo inicial, o hilo Main: arranca la ventana y termina.
  • El hilo Event-Dispatch Thread (EDT): se inicia cuando se hace visible la ventana.
  • Algunos Worker Threads: hilos en segundo plano con cargas de trabajo pesadas.

Si en una aplicación con GUI voy a realizar tareas de larga carga de proceso debo entender el funcionamiento del hilo EDT:

Event-Dispatch Thread (EDT) e hilos Worker

Todo el código de manejo de eventos, de visualización, y de actualización de componentes gráficos es ejecutado por Java en el Event-Dispatch Thread, el cual está diseñado para realizar operaciones cortas en respuesta a eventos.

Figure 7: Ciclo de vida de un evento generado al pulsar un JButton en un JFrame

Al ejecutar todos los manejos de eventos en el mismo hilo, Java se asegura que el bloque de operaciones en respuesta a un evento se ejecuta completamente antes de atender al siguiente evento. Si al responder a un evento, nuestro programa realiza tareas de larga ejecución, la ventana se congelará y no responderá hasta que el EDT termine de realizar dicha operación.

Figure 8: Ejecución de tareas pesadas en el hilo EDT

Para diseñar aplicaciones con GUIs Swing que sean Thread-Safe debo asegurar que toda intrucción que acceda o modifique el estado de la interfaz gráfica se ejecute en el hilo EDT2). Mientras que el acceso o modificación de componentes Swing se produzcan en los bloques de los listeners, no tendremos problema.

Podemos comprobar si un bloque de código se está ejecutando en el EDT, llamando al método isEventDispatchThread():

   // Muestro la respuesta por consola a modo de comprobación
   System.out.println(SwingUtilities.isEventDispatchThread());

En resumen, nuestras aplicaciones con GUI deben respetar estas dos premisas:3)

  1. Las operaciones con alta carga de ejecución no deben ser ejecutadas en el hilo EDT, para no bloquear la GUI.
  2. Las operaciones que accedan o modifiquen componentes gráficos de swing, siempre deben ser ejecutadas en el EDT.
Figure 9: Ejecución de tareas pesadas en hilo diferente a EDT

Por lo tanto, como programador:

  • Debo mantener las operaciones de un listener lo más sencillas posibles, creando un hilo (worker thread) para ejecutar cualquier tarea lenta (tareas lentas, accesos a ficheros, sockets, conexiones a bbdd, etc).
  • Si desde un hilo worker con una tarea de larga duración, necesito modificar el estado de componentes de Swing, debo enviar esas tareas de modificación a la cola del EDT.

El problema puede surgir cuando realizamos modificaciones sobre la GUI desde hilos creados por nosotros, ya que estas operaciones dejarían de ejecutarse en el EDT. Para controlar que las operaciones sobre la GUI se ejecutan en el hilo destinado a tal fin (EDT) podemos usar:

Método invokeLater()

Si en mi aplicación he implementado un hilo para realizar una tarea pesada y debo realizar operaciones que acceden o actualizan el estado de algún componente de la GUI, puedo añadir esa pequeña operación de acceso a la GUI a la cola de eventos para asegurarme que esas instrucciones se ejecuten en el EDT:

Figure 10: Llamada a invokeLater() desde una tarea pesada
private JProgressBar pbProgreso;
private JLabel lblContador;
. . .
 
// Método que responde a un evento sobre un botón
@Override
public void actionPerformed(ActionEvent e) {
   //Tarea en segundo plano de larga duración
   Thread hilo = new Thread(new Runnable() {
      @Override
      public void run() {
         for(int i = 1; i < tiempo; i++){
            Thread.sleep(500);
            //Aseguro que la modificación de la GUI se hace en EDT
            actualizarGUI(i, tiempo);
         }
      }
   });
}
. . .
private void actualizarGUI(int cuenta, int total){
   SwingUtilities.invokeLater(new Runnable() {
      @Override
      public void run() {
         lblContador.setText("Contador: " + cuenta);                  
         pbProgreso.setValue(cuenta * 100 / total);
      }
   });
}

El método actualizarGUI() realiza lo siguiente:

  1. llama a invokeLater()
  2. el método invokeLAter() crea una tarea Runnable y la añade a la cola del hilo EDT
  3. el método invokeLater() termina su ejecución
  4. el hilo EDT ejecutará dicha tarea (modificar los dos componentes Swing) cuando le llegue su turno

Si programamos adecuadamente una GUI, las tareas del EDT serás cortas y rápidas, y la respuesta será inmediata.

Clase SwingWorker

En algunas situaciones puede que el método invokeLater() no sea tan cómodo de utilizar, como cuando realizamos operaciones de larga duración. La clase SwingWorker crea un hilo de ejecución, que nos permite acceder de forma segura a la GUI.

En aplicaciones con GUIs, varios hilos pueden actuar sobre el mismo elemento gráfico (una etiqueta de texto, por ejemplo) por lo que varios hilos compartirán ese recurso. Así, la clase SwingWorker garantiza que el acceso a ese recurso compartido se hace de forma thread-safe, realizando las operaciones de acceso a la GUI en el Event Dispatch Thread.

Figure 11: Ejecución de una tarea mediante la clase SwingWorker

La clase SwingWorker tiene los siguientes métodos:

  • doInBackground(): Método que realiza la tarea pesada del hilo. Permite llamar al método publish(). Es el único método obligatorio, ya que es abstracto.
  • done(): método que se ejecuta en el EDT, al terminar la ejecución de doInBackGround().

Si queremos además contemplar el progreso de la tarea tenemos:

  • publish(): para enviar valores desde doInBackground(), con los que actualizar componentes de swing.
  • process(): método que se ejecuta en el EDT, recibe una lista con los valores enviados por publish(). Se ejecuta regularmente.

Visto esto, podemos actualizar componentes GUI al terminar la tarea (done()) y durante el progreso de la misma (process()).

Además el método doInBackground() permite devolver un objeto, por ejemplo el resultado de una carga de ficheros, o de una transferencia.

public class Tarea extends SwingWorker <Void, Integer> { 
  private JProgressBar barraProgreso;
  private JLabel labelEstado;
  // Se pasa como parametro componentes swing
  public Tarea(JProgressBar barraProgreso, JLabel labelEstado) { 
    this.barraProgreso = barraProgreso;
    this.labelEstado = labelEstado;
  }
 
  @Override
  protected Void doInBackground() throws Exception {
    // realiza una tarea pesada mediante un bucle (p.e. copiar un fichero)
    while(...){
       . . .
       // Se notifica el avance (valor entre 0 y 100)
       publish(avanceCarga); 
    }
    return null;
  }
 
  @Override
  protected void process(List<Integer> valores) {
    // En la lista valores obtengo lo que envía publish()
    // Se actualiza en el EDT de forma automática
    barraProgreso.setValue(valores.get(0));
  } 
 
  @Override
  protected void done() {
     // Se ejecuta en el EDT al terminar doInBackground()
     labelEstado.setText("Fichero copiado");
  }
}
 
public class ProgramaGUI { 
  . . .
  // Lanza la ejecucion de la tarea en segundo plano 
  // También le paso los 2 componentes GUI que quiero actualizar en el EDT
  Tarea tarea = new Tarea(barra, label); 
  tarea.execute(); 
}

Parar hilo Worker

En caso de querer finalizar las tareas del hilo worker por nuestra cuenta, podemos hacer uso del método cancel(true), el cual modifica el estado de ejecución de la tarea. Esto no detiene el hilo Worker directamente, sino que modificará un flag de estado que tiene la clase SwingWorker. Para comprobar el estado desde nuestra clase Worker, usaremos el método isCancelled().

. . .
@Override
  protected Void doInBackground() throws Exception {
    // además de controlar el progreso, comprobamos se se ha llamado a cancel()
    while(... && !isCancelled()){
       . . .
       //Realizo las tareas
    }
    return null;
  }
. . .
 
public static void main(String[] args){
   TareaWorker tarea = new TareaWorker();
   tarea.execute();
   . . .
   tarea.cancel(true);
}

Debemos de tener en cuenta que al cancelar la iteración de un bucle, el método doInBackground() terminará su ejecución. Y después se ejecutará también el método done(), independientemente de si se ha llamado a cancel().

Clase SwingWorker

Property Change Listener

Es un manejador de eventos que puedo vincular a cualquier tipo de objeto del que quiera ser notificado ante algún cambio en su estado. En este caso voy a ver cómo puedo hacer que mi programa principal se entere de los cambios producidos en mi objeto SwingWorker, para poder actualizar la GUI desde un manejador de eventos que se ejecuta en el EDT.

La clase SwingWorker tiene dos propiedades4), cuyos cambios serán notificados al PropertyChangeListener:

  • La propiedad progress: se modifica mediante el método setProgress() de la clase SwingWorker y admite valores entre 0-100.
  • La propiedad state: indica el estado del objeto SwingWorker:
    • STARTED: Es el estado del SwingWorker cuando ejecuta doInBackground()
    • PENDING: Es el estado previo al inicio de doInBackground()
    • DONE: Es el estado que tiene cuando ha terminado doInBackground().

Además, SwingWorker nos permite generar eventos PropertyChangeEvent en cualquier otro momento usando el método: firePropertyChange()

Notificar cambios en un objeto:

public class Tarea extends SwingWorker <Void, Integer> {
 
  @Override
  public Void doInBackground() throws Exception {
    while(condicion){
       // Tarea pesada
       . . .
       // Se notifica el avance modificando la propiedad "progress"
       setProgress(avanceCarga); 
    }
    //Si la tarea se cancela sin llegar a terminar, creo un evento
    if(!isCancelled()){
       firePropertyChange("cancelada", false, true);
    }
  }
}
. . .
public class ProgramaGUI {
  . . .
  Tarea tarea = new Tarea();
  tarea.addPropertyChangeListener(new PropertyChangeListener() {
    @Override
    public void propertyChange(PropertyChangeEvent event) {
      if (event.getPropertyName().equals("progress")) {
        int valor = (Integer) event.getNewValue();
        // Pintar este valor en barra de progreso o similar
      }
      else if (event.getPropertyName().equals("cancelada")) {
        //Tarea cancelada: notificarlo en la GUI
      } 
    }
  });
  tarea.execute();
}
Actualizar GUI con PropertyChangeListener

Ejercicios

  1. Escribe una aplicación que sirva de Alarma. El usuario podrá fijar una hora en la que tendrá que saltar un mensaje. En cualquier momento, podrá cancelarse la alarma
  2. ¿Cómo harías la aplicación anterior para que se puedan fijar varias Alarmas simultáneas a distintas horas?
  3. ¿Y si necesitas que una aplicación compruebe algo a intervalos regulares de tiempo? Por ejemplo, si hay actualizaciones, auto-guardar un documento, . . .

Ficheros de Registro (Logs)

A continuación se muestra, utilizando la librería log4j, un ejemplo de aplicación donde se realizan una serie de trazas a lo largo de su ejecución. La aplicación está compuesta por las dos clases que se muestran a continuación, con el objetivo de mostrar la traza cuando son varias clases las que ejecutan código.

public class Aplicacion {
 
  private static final Logger logger = LogManager.getLogger(Aplicacion.class);
 
  public static void main(String args[]) {
 
    // Diferentes niveles de traza
    logger.trace("Aplicación iniciada");
    logger.error("Error de algo");
    logger.trace("Aplicación finalizada");
    logger.debug("Información para depurar");
    logger.warn("Esto es un aviso");
 
    OtraClase unObjeto = new OtraClase();
    unObjeto.unMetodo();
 
    try {
      // Forzamos una excepción para registrar su traza con log4j
      int x = 5 / 0;
    } catch (Exception e) {
      logger.trace("Se ha producido una excepción");
 
      // Almacena la traza de la excepción como String y lo registra con log4j
      StringWriter sw = new StringWriter();
      e.printStackTrace(new PrintWriter(sw));
      logger.error(sw.toString());
      }
  }
}
public class OtraClase {
 
  private static final Logger logger = LogManager.getLogger(OtraClase.class);
 
  public void unMetodo() {
 
    logger.trace("Se ha ejecutado el método unMetodo");
  }
}

Antes de poder ejecutar la aplicación, se ha creado un fichero mínimo de configuración para log4j creando el siguiente fichero XML donde se habilita la traza por Consola y Fichero con un patrón de mensaje determinado

log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
  <Appenders>
    <Console name="Console" target="SYSTEM_OUT">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </Console>
    <File name="Fichero" fileName="ejemplolog4j.log" bufferedIO="false" advertiseURI="file://ejemplolog4j.log" advertise="true">
      <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
    </File>
  </Appenders>
  <Loggers>
    <Root level="trace">
      <AppenderRef ref="Console"/>
      <AppenderRef ref="Fichero"/>
    </Root>
  </Loggers>
</Configuration>

Así, para el ejemplo anterior, la traza resultante (tanto para consola como para el fichero ejemplolog4j.log) sería la siguiente

10:46:27.049 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Aplicación iniciada
10:46:27.051 [main] ERROR com.sfaci.ejemplolog4j.Aplicacion - Error de algo
10:46:27.051 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Aplicación finalizada
10:46:27.051 [main] DEBUG com.sfaci.ejemplolog4j.Aplicacion - Información para depurar
10:46:27.051 [main] WARN  com.sfaci.ejemplolog4j.Aplicacion - Esto es un aviso
10:46:27.051 [main] TRACE com.sfaci.ejemplolog4j.OtraClase - Se ha ejecutado el método unMetodo
10:46:27.052 [main] TRACE com.sfaci.ejemplolog4j.Aplicacion - Se ha producido una excepción
10:46:27.052 [main] ERROR com.sfaci.ejemplolog4j.Aplicacion - java.lang.ArithmeticException: / by zero
	at com.sfaci.ejemplolog4j.Aplicacion.main(Aplicacion.java:38)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:483)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

Ejercicios

  1. Realiza una aplicación de consola que cuente hasta un número determinado (mostrando la secuencia por pantalla) utilizando dos hilos, de forma que cada uno de ellos cuente un rango de números

  2. Realiza una aplicación de consola que cuente hasta un número determinado (mostrando la secuencia por pantalla) utilizando un número determinado de hilos. La secuencia de números se repartirá a partes iguales entre todos los hilos de forma que a cada uno se le asigne un rango

  3. Realiza una aplicación que simule una carrera de coches (de hasta 4 coches). Para cada coche se podrá configurar su velocidad y en la aplicación podremos configurar la distancia del circuito. Una vez lanzada la carrera se irá mostrando por pantalla (mediante barras de progreso, por ejemplo) el desarrollo de la misma (el avance de cada coche en el tiempo). Al final de la carrera se anunciará el coche ganador y los demás se detendrán mostrando cuánta distancia han recorrido

  4. Realiza una aplicación en la que el usuario pueda programar una cuenta atrás que al terminar muestre un mensaje en la pantalla principal. Además, se mostrará en una barra de progreso el transcurso de dicha cuenta atrás (vaciando la barra de progreso)

  5. Realiza una aplicación en la que se muestre, mediante una barra de progreso y una etiqueta de texto, el tiempo que pasa, en segundos, hasta una cantidad que habrá introducido el usuario. En cualquier momento éste podrá cancelar la cuenta.

Proyectos de ejemplo

Todos los proyectos realizados en los videos y otros de ejemplo de este tema se pueden encontrar en el repositorio de hilos de Bibucket.

Los proyectos de los ejercicios que se vayan haciendo en clase estarán disponibles en el repositorio psp-ejercicios de BitBucket


Prácticas

  • Práctica 1.1 Creación de una aplicación multihilo

© 2024 Santiago Faci y Fernando Valdeón

1)
Por ejemplo, el proyecto SETI@home http://setiathome.berkeley.edu
3)
Indicado, tambien, en la documentación de la clase SwingWorker https://docs.oracle.com/javase/8/docs/api/javax/swing/SwingWorker.html
bloque1/hilos.txt · Última modificación: 2024/09/16 20:53 por 127.0.0.1