Comunicación entre Hilos en Java

Avanzado

Considera la siguiente situación. Un hilo llamado T se está ejecutando dentro de un método synchronized y necesita acceso a un recurso llamado R que no está disponible temporalmente. ¿Qué debería hacer T? Si T ingresa alguna forma de bucle de sondeo que espera a R, T ata el objeto, evitando el acceso de otros hilos a él. Esta es una solución menos que óptima porque parcialmente derrota las ventajas de la programación para un entorno multihilo.

Una mejor solución es hacer que T renuncie temporalmente al control del objeto, permitiendo que se ejecute otro hilo. Cuando R está disponible, se puede notificar a T y reanudar la ejecución. Tal enfoque se basa en alguna forma de comunicación entre hilos en la que un hilo puede notificar a otro que está bloqueado y recibir una notificación de que puede reanudar la ejecución. Java admite la comunicación entre hilos con los métodos wait(), notify() y notifyAll().

1. Métodos wait(), notify() y notifyAll()

Los métodos wait(), notify() y notifyAll() son parte de todos los objetos porque están implementados por la clase Object. Estos métodos solo deben invocarse desde un contexto sincronizado. Aquí te muestro cómo se usan.

Cuando un hilo se bloquea temporalmente para ejecutarse, llama a wait(). Esto ocasiona que el hilo quede en reposo y que se libere el monitor para ese objeto, permitiendo que otro hilo use el objeto. En un momento posterior, el hilo en reposo se activa cuando otro hilo entra al mismo monitor y llama a notify() o notifyAll().

A continuación se muestran las diversas formas de wait() definidas por Object:

final void wait( ) throws InterruptedException
final void wait(long millis) throws InterruptedException
final void wait(long millis, int nanos) throws InterruptedException

La primera forma espera hasta ser notificado. La segunda forma espera hasta que se lo notifique o hasta que expire el período especificado de milisegundos. La tercera forma le permite especificar el período de espera en términos de nanosegundos.

Aquí están las formas generales para notify() y notifyAll():

final void notify()
final void notifyAll()

Una llamada a notify() reanuda un hilo de espera. Una llamada a notifyAll() notifica a todos los hilos, con el hilo de mayor prioridad ganando acceso al objeto.

Antes de mirar un ejemplo que usa wait(), se necesita hacer un punto importante. Aunque wait() normalmente espera hasta que se llame a notify() o notifyAll(), existe la posibilidad de que, en casos muy raros, el hilo de espera se pueda activar debido a una falsa alarma.

Las condiciones que conducen a una activación falsa son complejas. Sin embargo, Oracle recomienda que, debido a la remota posibilidad de una activación falsa, las llamadas a wait() se realicen dentro de un bucle que verifique la condición en la que el hilo está esperando. El siguiente ejemplo muestra esta técnica.

2. Ejemplo del uso de wait() y notify()

Para comprender la necesidad y la aplicación de wait() y notify(), crearé un programa que simula el tic-tac de un reloj mostrando las palabras Tic y Tac en la pantalla.

Para lograr esto, crearemos una clase llamada TicTac que contiene dos métodos: tic() y tac(). El método tic() muestra la palabra “Tic”, y tac() muestra “Tac”. Para ejecutar el reloj, se crean dos hilos, uno que llama a tic() y otro que llama a tac(). El objetivo es hacer que los dos hilos se ejecuten de forma tal que la salida del programa muestre un “Tic Tac” consistente, es decir, un patrón repetido de un tic seguido de un tac.

//Uso de wait() y notify() para crear un reloj que haga tictac.
class TicTac{
    String estado; // contiene el estado del reloj

    synchronized void tic(boolean corriendo){
        if (!corriendo){//Detiene el reloj
            estado="ticmarcado";
            notify(); //notifica a los hilos en espera
            return;
        }
        System.out.print("Tic ");
        estado="ticmarcado";//establece el estado actual a marcado
        notify(); //deja que tac() se ejecute, tic() notifica a tac()

        try {
            while (!estado.equals("tacmarcado"))
                wait(); //tic() espera a que se complete tac()
        }catch (InterruptedException exc){
            System.out.println("Hilo interrumpido.");
        }
    }

    synchronized void tac(boolean corriendo){
        if (!corriendo){//Detiene el reloj
            estado="tacmarcado";
            notify(); //notifica a los hilos en espera
            return;
        }
        System.out.println("Tac");
        estado="tacmarcado";//establece el estado actual a marcado
        notify();//deja que tic() se ejecute, tac() notifica a tic()

        try {
            while (!estado.equals("ticmarcado"))
                wait(); //tac() espera a que se complete tic()
        }catch (InterruptedException exc){
            System.out.println("Hilo interrumpido.");
        }
    }
}

class MiNHilo implements Runnable{
    Thread hilo;
    TicTac ttob;

    MiNHilo(String nombre, TicTac tt){
        hilo=new Thread(this,nombre);
        ttob=tt;
    }

    public static MiNHilo crearEIniciar(String nombre, TicTac tt){
        MiNHilo miNHilo=new MiNHilo(nombre,tt);
        miNHilo.hilo.start(); //Inicia el hilo
        return miNHilo;
    }
    public void run(){
        if (hilo.getName().compareTo("Tic")==0){
            for (int i=0; i<5; i++) ttob.tic(true);
            ttob.tic(false);
        }else {
        for (int i=0; i<5;i++) ttob.tac(true);
        ttob.tac(false);
        }
    }
}
class ComHilos {
    public static void main(String[] args) {
        TicTac tt=new TicTac();
        MiNHilo mh1=MiNHilo.crearEIniciar("Tic",tt);
        MiNHilo mh2=MiNHilo.crearEIniciar("Tac",tt);

        try {
            mh1.hilo.join();
            mh2.hilo.join();
        }catch (InterruptedException exc){
            System.out.println("Hilo principal interrumpido.");
        }
    }
}

Aquí está la salida producida por el programa::

Tic Tac
Tic Tac
Tic Tac
Tic Tac
Tic Tac

2.1. Explicación del código (I)

Echemos un vistazo de cerca a este programa. El corazón del reloj es la clase TicTac. Contiene dos métodos, tic() y tac(), que se comunican entre sí para garantizar que un Tic siempre va seguido de un Tac, que siempre va seguido de un Tic, y así sucesivamente. Observe el campo de estado. Cuando el reloj se está ejecutando, el estado mantendrá la cadena “ticmarcado” o “tacmarcado“, que indica el estado actual del reloj. En main(), se crea un objeto TicTac llamado tt, y este objeto se usa para iniciar dos hilos de ejecución.

Los hilos se basan en objetos de tipo MiNHilo. Tanto el constructor MiNHilo como el método crearEIniciar() tienen dos argumentos. El primero se convierte en el nombre del hilo. Esto será “Tic” o “Tac”. El segundo es una referencia al objeto TicTac, que es tt en este caso. Dentro del método run() de MiNHilo, si el nombre del hilo es “Tic”, se realizan llamadas a tic(). Si el nombre del hilo es “Tac”, se llama al método tac(). Se hacen cinco llamadas que pasan “true” como un argumento a cada método. El reloj funciona mientras se pase true. Una llamada final que pasa false a cada método detiene el reloj.

2.2. Explicación del código (II)

La parte más importante del programa se encuentra en los métodos tic() y tac() de TicTac. Comenzaremos con el método tic(), que, por conveniencia, se muestra aquí:

synchronized void tic(boolean corriendo){
        if (!corriendo){//Detiene el reloj
            estado="ticmarcado";
            notify(); //notifica a los hilos en espera
            return;
        }
        System.out.print("Tic ");
        estado="ticmarcado";//establece el estado actual a marcado
        notify(); //deja que tac() se ejecute, tic() notifica a tac()

        try {
            while (!estado.equals("tacmarcado"))
                wait(); //tic() espera a que se complete tac()
        }catch (InterruptedException exc){
            System.out.println("Hilo interrumpido.");
        }
}

Primero, observe que tic() es modificado por synchronized. Recuerde, wait() y notify() se aplican solo a métodos sincronizados. El método comienza al verificar el valor del parámetro corriendo. Este parámetro se usa para proporcionar un apagado limpio del reloj. Si es false, entonces el reloj ha sido detenido. Si este es el caso, el estado está configurado como “ticmarcado” y se realiza una llamada a notify() para habilitar la ejecución de cualquier hilo en espera. Volveremos a este punto en un momento.

Suponiendo que el reloj está corriendo cuando se ejecuta tic(), se muestra la palabra “Tic”, el estado se establece en “ticmarcado”, y luego tiene lugar una llamada a notify(). La llamada a notify() permite que se ejecute un hilo en espera en el mismo objeto. A continuación, se llama a wait() dentro de un ciclo while. La llamada a wait() hace que tic() se suspenda hasta que otro hilo invoque notify(). Por lo tanto, el ciclo no se repetirá hasta que otro hilo invoque notify() en el mismo objeto. Como resultado, cuando se invoca tic(), muestra un “Tic”, permite que se ejecute otro hilo y luego se suspende.

El ciclo while que llama a wait() verifica el valor del estado, esperando que sea igual a “tacmarcado”, que será el caso solo después de que se ejecute el método tac(). Como se explicó, el uso de un ciclo while para verificar esta condición evita que un spurious wakeup reinicie incorrectamente el hilo. Si estado no es igual a “tacmarcado” cuando wait() retorna, significa que se produjo una activación falsa y simplemente se vuelve a llamar a wait().

2.3. Explicación del código (II)

El método tac() es una copia exacta de tic() excepto que muestra “Tac” y establece el estado en “tacmarcado”. Por lo tanto, cuando se ingresa, muestra “Tac”, llama a notify() y luego espera. Cuando se ve como una pareja, una llamada a tic() solo puede ser seguida por una llamada a tac(), que solo puede ser seguido por una llamada a tic(), y así sucesivamente. Por lo tanto, los dos métodos se sincronizan mutuamente.

El motivo de la llamada a notify() cuando se detiene el reloj es permitir que una llamada final a wait() tenga éxito. Recuerde, tanto tic() como tac(), ejecutan una llamada a wait() luego de mostrar su mensaje. El problema es que cuando se detiene el reloj, uno de los métodos seguirá esperando. Por lo tanto, se requiere una llamada final a notify() para que se ejecute el método de espera. Como experimento, intente eliminar esta llamada a notify() y ver qué sucede. Como verá, el programa se “colgará” y deberá presionar CTRL-C para salir. La razón de esto es que cuando la llamada final a tac(), llama a wait(), no hay una llamada correspondiente a notify() que permita concluir tac(). Por lo tanto, tac() simplemente se queda allí, esperando por siempre.

Antes de continuar, si tiene alguna duda de que las llamadas a wait() y notify() son realmente necesarias para que el “reloj” funcione correctamente, sustituya esta versión de TicTac en el programa anterior. Tiene todas las llamadas a wait()notify() eliminadas.

class TicTac{
    String estado; // contiene el estado del reloj

    synchronized void tic(boolean corriendo){
        if (!corriendo){//Detiene el reloj
            estado="ticmarcado";
            return;
        }
        System.out.print("Tic ");
        estado="ticmarcado";//establece el estado actual a marcado
    }

    synchronized void tac(boolean corriendo){
        if (!corriendo){//Detiene el reloj
            estado="tacmarcado";
            return;
        }
        System.out.println("Tac");
        estado="tacmarcado";//establece el estado actual a marcado
    }
}

Salida:

Tic Tic Tic Tic Tic Tac
Tac
Tac
Tac
Tac

Claramente, los métodos tic() y tac() ya no funcionan juntos.

Hilos en Java
  • Comunicación entre Hilos

Sobre el Autor:

Hey hola! Yo soy Alex Walton y tengo el placer de compartir conocimientos hacía ti sobre el tema de Programación en Java, desde cero, Online y Gratis.

Deja una Respuesta

*

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.