Claramente, notify
despierta (cualquier) un hilo en el conjunto de espera, notifyAll
despierta todos los hilos en el conjunto de espera. La siguiente discusión debería aclarar cualquier duda. notifyAll
debe usarse la mayor parte del tiempo. Si no está seguro de cuál usar, use notifyAll
. Consulte la explicación que sigue.
Lea con mucho cuidado y comprenda. Por favor envíeme un correo electrónico si tiene alguna pregunta.
Mire al productor / consumidor (la suposición es una clase ProducerConsumer con dos métodos). ESTÁ ROTO (porque usa notify
), sí, PUEDE funcionar, incluso la mayoría de las veces, pero también puede causar un punto muerto; veremos por qué:
public synchronized void put(Object o) {
while (buf.size()==MAX_SIZE) {
wait(); // called if the buffer is full (try/catch removed for brevity)
}
buf.add(o);
notify(); // called in case there are any getters or putters waiting
}
public synchronized Object get() {
// Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
while (buf.size()==0) {
wait(); // called if the buffer is empty (try/catch removed for brevity)
// X: this is where C1 tries to re-acquire the lock (see below)
}
Object o = buf.remove(0);
notify(); // called if there are any getters or putters waiting
return o;
}
PRIMERAMENTE,
¿Por qué necesitamos un bucle while que rodea la espera?
Necesitamos un while
bucle en caso de que tengamos esta situación:
El consumidor 1 (C1) ingresa el bloque sincronizado y el búfer está vacío, por lo que C1 se coloca en el conjunto de espera (a través de la wait
llamada). El consumidor 2 (C2) está a punto de ingresar el método sincronizado (en el punto Y anterior), pero el productor P1 coloca un objeto en el búfer y luego llama notify
. El único subproceso en espera es C1, por lo que se despierta y ahora intenta volver a adquirir el bloqueo del objeto en el punto X (arriba).
Ahora C1 y C2 están intentando adquirir el bloqueo de sincronización. Se elige uno de ellos (de manera no determinista) y se ingresa al método, el otro se bloquea (no espera, pero se bloquea, tratando de obtener el bloqueo del método). Digamos que C2 obtiene el bloqueo primero. C1 sigue bloqueando (tratando de adquirir el bloqueo en X). C2 completa el método y libera el bloqueo. Ahora, C1 adquiere la cerradura. Adivina qué, por suerte tenemos un while
bucle, porque, C1 realiza la verificación del bucle (guardia) y se le impide eliminar un elemento inexistente del búfer (¡C2 ya lo tiene!). Si no tuviéramos un while
, obtendríamos un IndexArrayOutOfBoundsException
mensaje cuando C1 intente eliminar el primer elemento del búfer.
AHORA,
Ok, ahora ¿por qué necesitamos notificar a todos?
En el ejemplo anterior de productor / consumidor, parece que podemos salirse con la suya notify
. Parece de esta manera, porque podemos demostrar que los guardias en los bucles de espera para el productor y el consumidor son mutuamente excluyentes. Es decir, parece que no podemos tener un hilo esperando tanto en el put
método como en el get
método, porque, para que eso sea cierto, lo siguiente debería ser cierto:
buf.size() == 0 AND buf.size() == MAX_SIZE
(suponga que MAX_SIZE no es 0)
SIN EMBARGO, esto no es lo suficientemente bueno, NECESITAMOS usarlo notifyAll
. Veamos por qué ...
Supongamos que tenemos un búfer de tamaño 1 (para que el ejemplo sea fácil de seguir). Los siguientes pasos nos llevan al punto muerto. Tenga en cuenta que en CUALQUIER MOMENTO un hilo se despierta con notificación, puede ser seleccionado de forma no determinista por la JVM, es decir, cualquier hilo en espera se puede despertar. También tenga en cuenta que cuando varios subprocesos están bloqueando la entrada a un método (es decir, tratando de adquirir un bloqueo), el orden de adquisición puede ser no determinista. Recuerde también que un hilo solo puede estar en uno de los métodos a la vez: los métodos sincronizados permiten que solo un hilo se ejecute (es decir, mantener el bloqueo de) cualquier método (sincronizado) en la clase. Si se produce la siguiente secuencia de eventos, se produce un punto muerto:
PASO 1:
- P1 pone 1 char en el búfer
PASO 2:
- Intentos de P2 put
- comprueba el ciclo de espera - ya es un char - espera
PASO 3:
- Intentos de P3 put
- comprueba el ciclo de espera - ya es un char - espera
PASO 4:
- C1 intenta obtener 1 char
- C2 intenta obtener 1 char - bloques al ingresar al get
método
- C3 intenta obtener 1 char - bloques al ingresar al get
método
PASO 5:
- C1 está ejecutando el get
método - obtiene el método char, llama notify
, sale
- notify
P2 se despierta
- PERO, C2 ingresa al método antes de que P2 pueda (P2 debe recuperar el bloqueo), por lo que P2 se bloquea al ingresar al put
método
- C2 comprueba el bucle de espera, no hay más caracteres en el búfer, por lo que espera
: C3 ingresa al método después de C2, pero antes de P2, comprueba el bucle de espera, no hay más caracteres en el búfer, por lo que espera
PASO 6:
- AHORA: ¡hay P3, C2 y C3 esperando!
- Finalmente P2 adquiere el bloqueo, pone un char en el búfer, llama a notificar, sale del método
PASO 7:
- La notificación de P2 despierta a P3 (recuerde que cualquier subproceso se puede despertar)
- P3 verifica la condición del bucle de espera, ya hay un carácter en el búfer, por lo que espera.
- ¡NO HAY MÁS HILOS PARA LLAMAR NOTIFICACIÓN y TRES HILOS SUSPENDIDOS PERMANENTEMENTE!
SOLUCIÓN: Reemplace notify
con notifyAll
en el código de productor / consumidor (arriba).