Las funciones wait(), notify() y notifyAll() interactúan con el monitor de una instancia de una clase Java. Esta es la razón por la que forman parte de la definición de Object, porque el monitor se define a nivel de Object.
Estas funciones pueden resultar complicadas de comprender al principio debido a que mezclan varios conceptos que inicialmente puede parecer que no tienen que ver, porque trabajan a la vez con monitores y con hilos.
El método wait()
Cuando se llame al método wait() de un objeto, el hilo que haga la llamada se suspenderá y dejará de recibir tiempo de CPU.
El secreto está en que para poder llamar a wait(), el objeto del que llamas a ese método tiene que tener ahora mismo su monitor poseído por el hilo que está llamando a ese wait().
Esto quiere decir que no puedes poner una llamada a cualquierCosa.wait() en medio del código. Debes primero poseer el monitor del objeto respecto al que tratas de llamar a wait(). Por lo tanto, únicamente podrás llamar a este método dentro de un bloque o método synchronized.
En el siguiente ejemplo, llamamos a this.wait() dentro de un método synchronized. Esto funciona porque para poder entrar a un método marcado así, es necesario que el hilo bloquee el monitor asociado a la instancia cuyo método se está llamando (this):
synchronized void recibir() {
this.wait();
}
También podrías llamar al método wait() de un objeto que sea protagonista de una región de código crítica declarada con el bloque synchronized:
synchronized (queue) {
queue.wait();
}
Una consecuencia importante de llamar a wait() es que, como el hilo suspende su ejecución, provoca que el monitor se libere inmediatamente. Esto resulta raro porque la llamada al método no ha terminado, o bien todavía no se ha salido de la región crítica. Sin embargo, debido a que el hilo se suspende, no tiene sentido que posea el monitor. Lo liberará para que otro hilo lo pueda tomar.
El método notify()
Quizá te preguntes por qué es tan importante que para suspender un hilo haya que utilizar un monitor, en vez de simplemente llamar a algún método específico de la clase Thread al que se pudiese acceder desde Thread.currentThread().
Utilizar un monitor para suspender un proceso puede parecer como utilizar el interruptor de la lámpara para que salga agua caliente de un grifo. Sin embargo, la razón por la que se hace esto se entenderá cuando explique para qué sirve el método notify().
Una vez que un hilo ha sido suspendido debido a llamar al método wait(), otro hilo podrá reactivarlo utilizando el método notify(). Aquí la clave es que tiene que ser otro hilo el que despierte al hilo suspendido. Por eso, usar wait() y notify() sólo tiene sentido cuando un programa es multihilo y hay al menos dos hilos para participar en esta operación.
Cuando llames al método notify() de un objeto, uno de los hilos que se encuentre en ese momento suspendido por haber llamado al método wait() de precisamente esa misma instancia será despertado y marcado como listo para ejecutar.
Para poder utilizar este método, el hilo también necesita haber capturado el monitor, así que también es obligatorio utilizarlo en una región de código sincronizada, sea de método o de bloque.
synchronized void enviar() {
this.notify();
}
O bien,
synchronized (queue) {
queue.notify();
}
Ambas operaciones son legales. Recuerda que el otro hilo, que ahora mismo está dormido, soltó el monitor de su bloque synchronized cuando se llamó al método wait(). Esto es lo que permite que ahora haya otro hilo separado que pueda precisamente reservar el mismo monitor para su región crítica.
El hilo que pase a listo para seguir ejecutándose tendrá que esperar a que llegue su turno para ejecutarse. Y cuando se le vuelva a entregar tiempo de CPU, continuará su ejecución desde el punto en el que lo dejó, por lo que concluirá la llamada a wait().
Esto, no olvidemos, se hace dentro de una zona marcada como synchronized, para un monitor que ahora mismo no sólo no posee, sino que está tomado por otro hilo (el que ha llamado a notify()). Por esta razón, el hilo que previamente estaba a la espera tendrá que terminar a que el hilo que actualmente posee el monitor termine de ejecutar su zona sincronizada para tener posibilidades de ejecutarse.
Esta es la razón por la que se usan monitores como objeto indirecto para esta operación de dormir y despertar, porque son los monitores los que permiten asociar una instancia con un hilo que se haya suspendido a través de esa instancia. En otras palabras, si quieres reactivar un hilo, tendrás que hacerlo a través de la misma instancia cuyo monitor se usó para que se inactivase en primer lugar.
Los spurious wakeups y cómo remediarlos
Por contraintuitivo que parezca, a veces un hilo podría despertarse de un wait() sin que se haya llamado a notify() en otro.
Esto es algo que pasa con más lenguajes de programación y más bibliotecas multihilo, pero que en el caso de Java puede ocurrir debido a la forma en la que se implementa a nivel sistema operativo el par de funciones wait/notify.
No es insomnio, sino más bien que algunos sistemas operativos podrían conectar todos los hilos marcados como a la espera, a la misma señal, por lo que mandar una señal a uno de los hilos para despertarlo podría provocar que todos los hilos conectados a la misma señal se despierten, aunque no tengan que ver con el programa.
Suena a bug, y parece bug, pero es una decisión arquitectónica de algunos sistemas operativos. Por lo tanto, es bastante probable que no tengas la capacidad de hacer nada para impedir esto más que tratar un spurious wakeup (que es como se llama este efecto) de forma cobarde.
Generalmente, poner una llamada a wait() suelta dentro de una región sincronizada no va a ser suficiente, porque si la ejecución del código continúa cuando no le toca, podrían provocarse errores de ejecución críticos si justo después del wait() viene una parte de código que todavía no le tocaba ejecutar. Por ejemplo, imagina que después del wait() el hilo trata de leer de una zona de memoria compartida que todavía no ha sido preparada.
Lo normal será que se envuelva ese wait() en algún tipo de bucle, que evalúe si la condición por la cual estamos haciendo el wait() ya no se está cumpliendo, o si por el contrario es necesario volver a dormir.
En el siguiente ejemplo, este código es frágil:
void deliver() {
synchronized (queue) {
queue.wait();
server.send(queue.get(0));
}
}
La razón es que si el hilo se despierta súbitamente antes de tiempo, y antes de que otro hilo haya podido agregar algo a la cola y notificarle, posiblemente el programa falle por tratar de acceder a una cola que ahora mismo está vacía. Una forma de hacer más resiliente este programa sería ponerlo a dormir siempre que la cola esté vacía.
void deliver() {
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait();
}
server.send(queue.get(0));
}
}
El método notifyAll()
Múltiples hilos podrían llamar a wait() usando el mismo monitor. Esto ocurre porque, como ya he dicho varias veces a estas alturas, cuando un hilo llama al método wait() de una instancia, suelta el monitor que tiene que reservar para poder llamar al método en primer lugar, por lo que dependiendo de las necesidades del programa, sería perfectamente aceptable que otro hilo aproveche para capturar el monitor y entrar otra vez en una zona sincronizada que llame a wait() sobre ese mismo monitor.
El caso más directo sería que varios hilos llamen al mismo método marcado como synchronized, y que este método llame a this.wait().
En estos casos, lo que tendríamos son múltiples hilos a la espera, todos asociados a la misma instancia. Eso significa que los hilos se despertarían llamando a notify() desde otro hilo que reserve el monitor.
Cuando llamas a notify(), la máquina virtual de Java selecciona uno de los hilos que estén asociados al monitor y lo reaviva. Sin embargo, en caso de que haya más de uno, cuál se elige es una operación indefinida. Es posible que seleccione el hilo que más tiempo lleve dormido, o es posible que seleccione el hilo que más afinidad tenga en ese momento. Sin embargo, no está claro cuál será despertado y no hay forma de configurar esto. Todo lo que es certero es que una llamada a notify() equivale a reavivar un hilo y sólo un hilo.
Por lo tanto, si varios hilos están a la espera y asociados al mismo monitor, tendrías que llamar múltiples veces a notify(), una por hilo. Si haces N llamadas, se despertarán N hilos.
La otra alternativa es simplemente usar notifyAll(). Al usar notifyAll(), todos los hilos que previamente hayan quedado inactivos se activarán a la vez. Aplican las mismas semánticas que notify(). En otras palabras, es necesario llamar a este método desde un método marcado como synchronized.