HilosJavaBasico#

System.out.println("Hola Mundo desde Notebook")

Hilos en Java#

Los hilos son una característica importante en la programación concurrente, y permiten que un programa ejecute múltiples tareas al mismo tiempo. En Java, esto se logra mediante la clase ‘Thread’ y la interfaz ‘Runnable’.

1. La clase Thread:#

Java proporciona una clase incorporada llamada Thread que se encuentra en el paquete java.lang. Puedes crear un hilo extendiendo esta clase e implementando el método run().

class MiHilo extends Thread {
    public void run() { 
        try { // se debe implementar esto ya que no se permiten colocar sleeps si no se incrustan dentro de excepciones.
            System.out.println("Hola Mundo desde un Hilo heredado");
            Thread.sleep(2000);
        } catch (Exception e) { // Si se da una excepción de algun tipo, se imprime el mensaje de interrupción.
            System.out.println("Ejecución interrumpida");
        }
    }
}

Iniciar un hilo#

Para inciar un hilo primero debes instanciar la clase MiHilo.

Para ejecutar el hilo debes llamar el método Start directamente en tu objeto. Este ejecutará el procedimiento run.

public class Clase {
    public static void main() {
        // Crear una instancia de MiHilo
        MiHilo miHilo = new MiHilo();

        System.out.println("Hola Mundo desde El main");
        // Si extiende la clase Thread
        miHilo.start();

        System.out.println("Fin de la ejecución");
    }
}
Clase.main(); 
Hola Mundo desde El main
Fin de la ejecución
Hola Mundo desde un Hilo heredado

2. La interfaz Runnable:#

Otra forma de crear un hilo es implementar la interfaz Runnable. Esta interfaz solo tiene un método llamado run(), que debes implementar en tu clase.

class MiHiloR implements Runnable {
    public void run(){
        

        try { // se debe implementar esto ya que no se permiten colocar sleeps si no se incrustan dentro de excepciones.
            System.out.println("Hola Mundo desde un Hilo desde interfaz");
            Thread.sleep(2000);
        } catch (Exception e) { // Si se da una excepción de algun tipo, se imprime el mensaje de interrupción.
            System.out.println("Ejecución interrumpida");
        }

    }
}

Iniciar un hilo#

Para inciar un hilo primero debes instanciar la clase MiHiloR de tipo Thread.

Para ejecutar el hilo debes llamar el método Start directamente en tu objeto. Este ejecutará el procedimiento run.

public class Clase2{
    public static void main() {
        System.out.println("Inicio Programa Main");
        MiHiloR Runnable1 = new MiHiloR();

        Thread hilo1 = new Thread(Runnable1);

        hilo1.start();

        System.out.println("Final del programa Main");
    }
}
Clase2.main(); // Tuvimos que parar de golpe la ejecución.
Inicio Programa Main
Final del programa Main
Hola Mundo desde un Hilo desde interfaz

Diferencias entre Threads y Runnable#

A continuación, se presentan las diferencias clave entre ambas:

  1. Herencia: Cuando extiendes la clase Thread, estás utilizando la herencia para crear un hilo. Esto significa que tu clase heredará todas las propiedades y métodos de la clase Thread. Sin embargo, Java no admite la herencia múltiple, lo que significa que si tu clase ya hereda de otra clase, no puedes extender Thread al mismo tiempo. En cambio, cuando implementas la interfaz Runnable, puedes heredar de otra clase y también crear un hilo al mismo tiempo, ya que Java admite la implementación de múltiples interfaces.

  2. Reutilización del código : La implementación de la interfaz Runnable es más flexible en términos de reutilización de código. Si extiendes la clase Thread, no puedes separar el código del hilo de la propia clase Thread. Por otro lado, cuando implementas la interfaz Runnable, puedes reutilizar la misma instancia de la clase que implementa Runnable en varios hilos.

  3. Sobrecarga: Al extender la clase Thread, cada hilo crea una nueva instancia de la clase y, por lo tanto, consume más memoria en comparación con la implementación de la interfaz Runnable. En el caso de Runnable, puedes crear múltiples hilos utilizando una única instancia de la clase que implementa Runnable, lo que resulta en una menor sobrecarga de memoria.

En general, se recomienda utilizar la interfaz Runnable en lugar de extender la clase Thread debido a las ventajas mencionadas. Sin embargo, en algunos casos específicos, como cuando es necesario sobrescribir otros métodos de la clase Thread, extender Thread puede ser la opción adecuada.

// Este ejemplo implementa un hilo que cuando se llama, imprime el nombre del proceso seguido de un número que se incrementa
// Luego de imprimirlo, espera 1 segundo y luego vuelve

class MyRunnable implements Runnable {
    private String name;
    private int wait_time;

    public MyRunnable(String name, int wait_time) {
        this.name = name;
        this.wait_time = wait_time;
    }

    @Override
    public void run() {
        System.out.println("Iniciando proceso"+name);
        for (int i = 0; i < 5; i++) {
            System.out.println(name + ": " + i);
            try {
                Thread.sleep(wait_time);
            } catch (Exception e) {
                System.out.println(name + " ha sido interrumpido.");
            }
        }
        System.out.println(name + " ha terminado.");
    }
}
public class Clase3{
    public static void main() {
        System.out.println("Ingresando al Main");
        MyRunnable runnable1 = new MyRunnable("Hilo 1",1000);

        MyRunnable runnable2 = new MyRunnable("Hilo 2",2000);

        Thread hilo1 = new Thread(runnable1);
        Thread hilo2 = new Thread(runnable2);

        hilo1.start();
        hilo2.start();

        try { // Por reglas de java el siguiente método se debe colocar en una exception para evitar
            hilo1.join(); // este método se utiliza para esperar que se terminen de ejecutar los procesos, 
                          // de lo contrario se sigue ejecutando el main mientras se ejecutan los hilos.
            hilo2.join();
        } catch (Exception e) {
            System.out.println("___");
        }

        System.out.println("Terminando el proceso Main");
    }
}
Clase3.main();
Ingresando al Main
Iniciando procesoHilo 2
Iniciando procesoHilo 1
Hilo 2: 0
Hilo 1: 0
Hilo 1: 1
Hilo 2: 1
Hilo 1: 2
Hilo 1: 3
Hilo 2: 2
Hilo 1: 4
Hilo 1 ha terminado.
Hilo 2: 3
Hilo 2: 4
Hilo 2 ha terminado.
Terminando el proceso Main

Sincronización de Hilos#

Uno de los problemas clave en la programación concurrente es garantizar que los hilos accedan a los recursos compartidos de manera segura y controlada. La sincronización en Java ayuda a prevenir condiciones de carrera y garantiza que solo un hilo pueda acceder a un recurso compartido a la vez. Puedes sincronizar bloques de código utilizando la palabra clave synchronized.

public synchronized void metodoSincronizado() {
    // Código sincronizado
}

Ejemplo#

// Este es el objeto contador al que vamos a acceder
class Contador {
    private int valor;

    public Contador() {
        this.valor = 0;
    }

    // Método sincronizado para incrementar el contador, con esto se evita que dos hilos lo accedan al mismo tiempo
    public synchronized void incrementar() {
        valor++;
    }
    // Método sincronizado para obtener el valor actual del contador
    public synchronized int getValor() {
        return valor;
    }
}
class Incrementador implements Runnable {
    // dentro de la clase que implementa la interfaz vamos a declarar un atributo de tipo contador
    private Contador contador;

    public Incrementador(Contador contador) { // En el constructor de la clase,  asignamos el objeto contador que debe ser enviado desde el main.
        this.contador = contador;
    }

    @Override 
    public void run() {
        for (int i = 0; i < 5; i++) {
            contador.incrementar();
            System.out.println("El hilo valor del contador es:"+contador.getValor());
        }
    }
}
public class Clase4 {
    public static void main() throws InterruptedException {
        System.out.println("Empezamos el programa main");
        Contador contador = new Contador();
        Incrementador incrementador = new Incrementador(contador);

        // Crear 5 hilos que incrementen el contador
        Thread[] hilos = new Thread[5];
        for (int i = 0; i < 5; i++) {
            hilos[i] = new Thread(incrementador);
            hilos[i].start();
        }

        // Esperar a que todos los hilos terminen
        for (int i = 0; i < 5; i++) {
            hilos[i].join();
        }

        System.out.println("Valor final del contador: " + contador.getValor());
    }
}
Clase4.main();
Empezamos el programa main
El hilo valor del contador es:2
El hilo valor del contador es:5
El hilo valor del contador es:6
El hilo valor del contador es:8
El hilo valor del contador es:9
El hilo valor del contador es:10
El hilo valor del contador es:4
El hilo valor del contador es:3
El hilo valor del contador es:1
El hilo valor del contador es:13
El hilo valor del contador es:14
El hilo valor del contador es:15
El hilo valor del contador es:12
El hilo valor del contador es:11
El hilo valor del contador es:18
El hilo valor del contador es:7
El hilo valor del contador es:20
El hilo valor del contador es:19
El hilo valor del contador es:17
El hilo valor del contador es:16
El hilo valor del contador es:23
El hilo valor del contador es:24
El hilo valor del contador es:22
El hilo valor del contador es:21
El hilo valor del contador es:25
Valor final del contador: 25

En este ejemplo, hay un objeto Contador que mantiene un contador y proporciona métodos para incrementarlo y obtener su valor. La clase Incrementador implementa Runnable y, en su método run, incrementa el contador 5 veces. En el método main, creamos 5 hilos que incrementan el contador simultáneamente.

Dado que el método incrementar() en la clase Contador es sincronizado, garantizamos que solo un hilo pueda acceder a él a la vez. Esto evita condiciones de carrera y asegura que el contador se incremente de manera correcta y segura. Si no utilizáramos la sincronización, podríamos obtener resultados incorrectos debido a las condiciones de carrera entre los hilos.

Otros métodos de la clase Thread.#

  • isInterrupted(): Comprueba si el hilo ha sido interrumpido. Este método devuelve true si el hilo ha sido interrumpido y false en caso contrario.

class MiHilo extends Thread {
    @Override
    public void run() {
        int i = 0;
        while (!isInterrupted()) {
            System.out.println("Ejecutando el hilo: " + getName() + ", iteración: " + i);
            i++;
            try {
                Thread.sleep(1000); // Hace que el hilo duerma durante 1 segundo
            } catch (Exception e) {
                interrupt(); // Establece el estado de interrupción del hilo
            }
        }
    }
}

public class Clase6 {
    public static void main() {
        
        MiHilo hilo = new MiHilo();
        hilo.start(); // Inicia la ejecución del hilo

        try {
            Thread.sleep(4000); // Hace que el hilo principal duerma durante 4 segundos
        } catch (Exception e) {
            e.printStackTrace();
        }

        hilo.interrupt(); // Interrumpe el hilo en ejecución
        System.out.println("Terminando la ejecución del hilo");
    }
}
Clase6.main();
Ejecutando el hilo: Thread-1, iteración: 0
Ejecutando el hilo: Thread-1, iteración: 1
Ejecutando el hilo: Thread-1, iteración: 2
Ejecutando el hilo: Thread-1, iteración: 3
Terminando la ejecución del hilo
  • setName(String name) y getName(): Establece y obtiene el nombre del hilo, respectivamente.

class MiHilo extends Thread {
    public MiHilo(String name) {
        setName(name);
    }

    @Override
    public void run() {
        System.out.println("Ejecutando el hilo: " + getName());
    }
}

public class Clase7 {
    public static void main() {
        MiHilo hilo1 = new MiHilo("Hilo 1");
        MiHilo hilo2 = new MiHilo("Hilo 2");

        hilo1.start(); // Inicia la ejecución del hilo1
        hilo2.start(); // Inicia la ejecución del hilo2

        
        try {
            hilo1.join();
            hilo2.join();
        } catch (Exception e) {
            System.out.println("__");
        }
        
    }
}
Clase7.main();
Ejecutando el hilo: Hilo 2
Ejecutando el hilo: Hilo 1
  • currentThread(): Devuelve una referencia al hilo en ejecución actual. Esto es útil cuando necesitas acceder a información o realizar acciones específicas en el hilo actual.

class MiHilo extends Thread {
    @Override
    public void run() {
        System.out.println("Ejecutando el hilo actual: " + currentThread().getName());
    }
}

public class Clase8 {
    public static void main() {
        MiHilo hilo1 = new MiHilo();
        MiHilo hilo2 = new MiHilo();

        hilo1.setName("Hilo 1");
        hilo2.setName("Hilo 2");

        hilo1.start(); // Inicia la ejecución del hilo1
        hilo2.start(); // Inicia la ejecución del hilo2

        try {
            hilo1.join();
            hilo2.join();
        } catch (Exception e) {
            System.out.println("__");
        }
    }
}
Clase8.main();
Ejecutando el hilo actual: Hilo 1
Ejecutando el hilo actual: Hilo 2
  • wait(): Se utiliza en la programación multihilo para coordinar la comunicación entre hilos

Este método solo se puede llamar dentro de un bloque sincronizado, es decir, el hilo que lo llama debe tener el bloqueo del objeto en el que se invoca wait().

  1. Cuando un hilo invoca wait(), libera el bloqueo del objeto y entra en un estado de espera. El hilo permanecerá en estado de espera hasta que otro hilo invoque el método notify() o notifyAll() en el mismo objeto.

  2. La ejecución del hilo se reanuda cuando se recibe una notificación (notify() o notifyAll()) en el objeto y el hilo puede volver a adquirir el bloqueo del objeto.

  3. Existen tres versiones del método wait():

  • wait(): Hace que el hilo actual espere hasta que otro hilo invoque el método notify() o notifyAll() en el objeto.

  • wait(long timeout): Hace que el hilo actual espere hasta que otro hilo invoque el método notify() o notifyAll() en el objeto, o hasta que transcurra un tiempo de espera específico (en milisegundos).

  • wait(long timeout, int nanos): Hace que el hilo actual espere hasta que otro hilo invoque el método notify() o notifyAll() en el objeto, o hasta que transcurra un tiempo de espera específico (en milisegundos y nanosegundos).

  1. El método wait() puede lanzar una InterruptedException si el hilo que lo invoca es interrumpido mientras espera.

Ejemplo#

class Buffer {
    private Integer contenido;
    private boolean disponible = false;

    public synchronized void poner(int valor) throws InterruptedException {
        while (disponible) {
           wait(); // Espera hasta que el consumidor consuma el contenido
        }
        contenido = valor;
        disponible = true;
        System.out.println("Producido: " + contenido);
        notify(); // Notifica al consumidor que el contenido está disponible
    }

    public synchronized void tomar() throws InterruptedException {
        while (!disponible) {
            wait(); // Espera hasta que el productor produzca el contenido
        }
        disponible = false;
        System.out.println("Consumido: " + contenido);
        notify(); // Notifica al productor que el contenido ha sido consumido
    }
}

class Productor implements Runnable {
    private Buffer buffer;

    public Productor(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            try {
                buffer.poner(i);
                Thread.sleep(1000); // Simula tiempo de producción
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

class Consumidor implements Runnable {
    private Buffer buffer;

    public Consumidor(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i < 10; i++) {
            try {
                buffer.tomar();
                Thread.sleep(500); // Simula tiempo de consumo
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

public class Clase9 {
    public static void main() {
        Buffer buffer = new Buffer();
         
        Productor productor = new Productor(buffer);
        Consumidor consumidor = new Consumidor(buffer);
        
        Thread hiloProductor = new Thread(productor);
        Thread hiloConsumidor = new Thread(consumidor);

        hiloProductor.start();
        hiloConsumidor.start();

        try {
            hiloProductor.join();
            hiloConsumidor.join();
        } catch (Exception e) {
            System.out.println("__");
        }
    }
}
  File "<ipython-input-1-dffafce0b58d>", line 1
    class Buffer {
                 ^
SyntaxError: invalid syntax
Clase9.main();
Producido: 1
Consumido: 1
Producido: 2
Consumido: 2
Producido: 3
Consumido: 3
Producido: 4
Consumido: 4
Producido: 5
Consumido: 5
Producido: 6
Consumido: 6
Producido: 7
Consumido: 7
Producido: 8
Consumido: 8
Producido: 9
Consumido: 9