Hilos

Conceptos básicos y cómo funcionan los hilos:

Los hilos comparten el espacio de memoria del usuario, es decir corren dentro del contexto de otro programa; y los procesos generalmente mantienen su propio espacio de direcciones y entorno de operaciones. Por ello a los hilos se les conoce a menudo como procesos ligeros.

Evita que la carga y ejecución de los procesos sean más pesadas. Los usamos para realizar programas concurrentes (que no es lo mismo que paralelos).

Qué son?

Una secuencia de código en ejecución dentro del contexto de otro proceso. No pueden ejecutarse solos y puede haber uno o varios hilos en cada uno.

A partir del hilo principal podemos crear el resto de hilos que se ejecutarán paralelamente.

Se pueden usar para hacer tareas simultáneas que no tengan que detenerse entre sí.

Por ejemplo, un programa que controla sensores en una fábrica, cada sensor puede ser un hilo independiente y recoge un tipo de información; y todos deben controlarse de forma simultánea.

Clases para crear hilos

Hay dos clases que forman parte del paquete java.lang. Como no se pueden usar al mismo tiempo hay que escoger una u otra.

Extendiendo la clase Thread

Debe sobreescribirse su método run() con las acciones a desarrollar.

También existen las clases start() y stop() pero ya no se usan ahora porque no son seguras.

Crear el objeto hilo es lo habitual:

NombreHilo h = new NombreHilo();
//Instanciamos la ejecución
h.start();

Ejemplo básico:

public class PrimerHilo extends Thread{
  private int x;
  PrimerHilo (int x){
    this.x=x;
  }
  public void run() {
    for (int i=0; i<x; i++){
      System.out.println("En el Hilo...");
    }
  }
}

Ejemplo con varios hilos, concretamente 4, ya que hay que contar el main como un hilo en proceso:

public class HiloEjemplo1 extends Thread {
  private int c; // contador de cada hilo
  private int hilo;

  // constructor
  public HiloEjemplo1(int hilo) {
      this.hilo = hilo;
      System.out.println("CREANDO HILO: " + hilo);
  }

  // metodo run
  public void run() {
      c = 0;
      while (c <= 5) {
          System.out.println("Hilo: " + hilo + " C = " + c);
          c++;
      }
  }

  public static void main(String[] args) {
      HiloEjemplo1 h = null;
      for (int i = 0; i < 3; i++) {
          h = new HiloEjemplo1(i + 1); // crear hilo
          h.start(); // iniciar hilo
      }
      System.out.println("3 HILOS CREADOS...");
  } // main
} // clase

El main suele estar en otra clase

Métodos sobre los hilos en Java

MétodoMisión
start()Hace que el hilo comience la ejecución; la máquina virtual de Java llama al método run() de este hilo.
boolean isAlive()Comprueba si el hilo está vivo.
sleep(long milisegundos)Hace que el hilo actualmente en ejecución pase a dormir temporalmente durante el número de milisegundos especificado. Puede lanzar la excepción InterruptedException.
run()Constituye el cuerpo del hilo. Es llamado por el método start() después de que el hilo apropiado del sistema se haya inicializado. Si el método run() devuelve el control, el hilo se detiene. Es el único método de la interfaz Runnable.
String toString()Devuelve una representación en formato cadena de este hilo, incluyendo el nombre del hilo, la prioridad, y el grupo de hilos.
long getId()Devuelve el identificador del hilo.
void yield()Hace que el hilo actual de ejecución pare temporalmente y permita que otros hilos se ejecuten.
String getName()Devuelve el nombre del hilo.
setName(String name)Cambia el nombre de este hilo, asignándole el especificado como argumento.
int getPriority()Devuelve la prioridad del hilo.
setPriority(int p)Cambia la prioridad del hilo al valor entero p.
void interrupt()Interrumpe la ejecución del hilo.
boolean interrupted()Comprueba si el hilo actual ha sido interrumpido.
Thread currentThread()Devuelve una referencia al objeto hilo que se está ejecutando actualmente.
boolean isDaemon()Comprueba si el hilo es un hilo Daemon.
setDaemon(boolean on)Establece este hilo como hilo Daemon, o como hilo de usuario.
void stop()Detiene el hilo. Este método está en desuso.

Vamos a trabajar con la InterruptedException para gestionar los diferentes hilos.

Se pueden trabajar los hilos de forma conjunta, es lo que se denomina un poolde hilos.

Un Daemon es un servicio en segundo plano que está siempre disponible, como puede ser en el SO la cola de impresión.

Ejemplo detallado

public class HiloEjemplo2 extends Thread {
  public void run() {
      System.out.println("Dentro del Hilo: " + this.getName() +
          ", Prioridad: " + this.getPriority() +
          ", ID: " + this.getId());
  }

  public static void main(String[] args) {
    HiloEjemplo2 h = null;
    for (int i = 0; i < 3; i++) {
        h = new HiloEjemplo2(); // crear hilo
        h.setName("HILO" + i); // damos nombre al hilo
        h.setPriority(i + 1); // damos prioridad
        h.start(); // iniciar hilo
        System.out.println("Informacion del " +
            h.getName() + ": " + h.toString());
    }
    System.out.println("3 HILOS CREADOS...");
  } // main
}// clase

Implementando la interfaz Runnable

public class Reloj extends Applet implements Runnable {
    // La clase implementa Runnable, por lo que debe definir el método run()
}

// Forma general de declarar un hilo implementando la interfaz Runnable
class NombreHilo implements Runnable {
    // Propiedades, constructores y métodos de la clase
    public void run() {
        // Acciones que lleva a cabo el hilo
    }
}

public class PrimerHiloR implements Runnable {
    private int x;

    PrimerHiloR(int x) {
        this.x = x;
    }

    public void run() {
        for (int i = 0; i < x; i++)
            System.out.println("En el Hilo... " + i);
    }
}

Suspender un hilo

Lo hacemos con el método sleep(). No es que se detenga, pero lo deja dormido unos milisegundos. Por otra parte con suspend() lo paramos durante un tiempo indeterminado hasta que lo reanudes. aunque es un método obsoleto y puede provocar un bloqueo de otros procesos. Los interbloqueos pueden provocar una situación parecida a la de un bucle infinito. Se puede reanudar un hilo con resume() desde otro hilo, que también está en desuso. Enviamos señales entre hilos para gestionarlos.

Métodos en uso para suspender hilos:

Para suspender de forma segura el hilo se debe introducir en el hilo una variable, por ejemplo suspender y comprobar su valor dentro del método run(), es lo que se hace en la llamada al método suspender. waitForResume() del ejemplo siguiente. El método requestSuspend() del hilo da valor true a la variable para suspender el hilo.

El método wait() hace que el hilo espere hasta que le llegue un notify() o un notifyAII();

Wait() solo se puede llamar desde dentro de un método sincronizado (synchronized). Estos tres métodos se usan en sincronización de hilos.

Básicos sobre los hilos

Lo complicado es ver con las especificaciones cuando vamos a modelar los programas para usar los hilos

Con los simuladores que vamos a usar intentaremos crear programas de simulación que trabajen con hilos, de forma que cada hilo sea una entidad que maneje el programa y que se pueda ejectuar de una forma u otra las diferentes operaciones.

Interrupción de hilos

Usamos el método isInterrupted() para consultar si un hilo está interrumpido. Ojo con los booleanos de interrupted o isInterrupted ya que uno se queda fijo en true y el otro inmediatamente vuelve a false.

Public class Main{
  public static void main(String[] ars) {
    HiloUno uno = new HiloUno();

    Thread dos = new Thread(new HiloDos());

    uno.start();
    dos.start();

    // Dormimos un tiempo el hilo para evitar que se bloquee rápido
    Thread.sleep(Duration.ofSeconds(5));
    
    uno.interrupt();
    
    System.out.println("Fin de main");
  }
}

Public class HiloUno extends Thread {
  @Override
  public void run(){
    long i = 0;
    for(;;){

      // En este caso el boolean se usa una sola vez y se queda en true
      // si no alicaramos luego el return seguriía diciendo que se ha interrumpido
      boolean inter = isInterrupted();

      // Aquí señalaría cada vez que ocurre porque luego vuelve el booleano 
      // a false...
      boolean inter2 = Thread.interrupted();
      
      if (inter) {
        System.out.println("He sido ninterrumpido");
        return;
      }
      if(++i == 100000000){
        i = 0;
        System.out.println("Hola desde hilo Uno");
      }
    }
  }
}

public class HiloDos implements Runnable {
  @Override
  public void run() {
    System.out.println("Hola desde HiloDos");
  }
}

Unión de hilos con Thread.join()

Para que un hilo espere a la finalización de otro, debe usarse la llamada a join(). Por ejemplo esperar a que una imagen esté correctamente procesada antes de comentarlo.

public class Main {
  public static void main(String[] args) {
    // Aquí lo llamamos con funciones lambda
    Thread t = new Thread(() -> {
      try {
        System.out.println("Iniciamos");
        Thread.sleep(Duration.ofSeconds(2));
        System.out.println("Terminamos.");
      } catch (InterruptedException ex){
        Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null);
      }
    });

    t.start();

    // Si ponemos join con una duración y t no ha finalizado, el principal 
    // continuaria. Si no, se congela el main hasta terminar el t
    // es útil porque deja congelado el hilo y libera los recursos
    try{
      t.join();
    } catch (InterruptedException ex) {
      Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null);
    }
   System.out.println("Listo");
  }
}

Como usar Thread.join

Thread t = new Thread(() -> {
  System.out.println("Iniciamos");
  Thread.sleep(Duration.ofSeconds(2));
  System.out.println("Terminamos");
} catch (InterruptedException ex){
  Logger.getLogger(Main.class.getName().log(level.SEVERE,null));
});

t.start();

// Esta solución para mostrar el estado es ineficiente. El hilo sigue activo
// pero quita tiempo de CPU solo para ver cuando va a finalizar
while(t.isAlive){
  // Este mensaje puede llegar a mostrarse una vez más antes de mostrar el otro mensaje
  System.out.println("Todavía se está ejecutando");
}
System.out.println("Ya está listo")

// La forma ideal es hacerlo con join que no tiene tiempo de CPU hasta que el hilo
//t se termine
try{
  t.join();
  // Esto sirve para cuando esto falla, es decir el join no el t.
} catch (InterruptedException ex){
  Logger.getLogger(Main.class.getName().log(level.SEVERE,null));
}

El hilo t empieza, se pausa dos segundos y continua. Durante el uso de este hilo hubo dos hilos al mismo tiempo: el main y el Thread t.

Join con parámetros

Se puede usar join() con parámetros, aunque parece ser poco fiable. Puede usarse para desistir o para pasar algún mensaje al usuario si dura demasiado el hilo y no continua el programa por ello. El hilo sigue, pero el join() ejecuta el código siguiente a donde tenía que esperar.

t.join(5000); // Son milisegundos
t.join(Duration.ofSeconds(10)); // Segundos

Sección Crítica

Una sección crítica son un conjunto de acciones que deben tener garantizado su acceso en un orden concreto. Suele estar asociado a un recurso compartido para evitar que entren valores raros.

Corrupción de memoria cuando dos hilos concurren a la misma memoria sin comprobarla y eso da errores. La forma de organizarlos suele ser con los semáforos.

Ejemplo


Variable valor y tenemos un hilo que lo incrementa en 1. Imaginemos que otro hilo también lo haga. En un caso idílico ocurriría así.

Pero ¿qué ocurre si justo cuando el hilo uno está incrementando el dos accede al valor de la memoria que tenía el mismo valor 0? Al no haber control de acceso se quedó el valor en 1.


image image