¿Cuál es el daño potencial si fuera posible invocar wait()
fuera de un bloque sincronizado, conservando su semántica, suspendiendo el hilo de la persona que llama?
Vamos a ilustrar qué problemas tendríamos si wait()
pudiéramos llamarnos fuera de un bloque sincronizado con un ejemplo concreto .
Supongamos que implementamos una cola de bloqueo (lo sé, ya hay una en la API :)
Un primer intento (sin sincronización) podría verse algo en la línea de abajo
class BlockingQueue {
Queue<String> buffer = new LinkedList<String>();
public void give(String data) {
buffer.add(data);
notify(); // Since someone may be waiting in take!
}
public String take() throws InterruptedException {
while (buffer.isEmpty()) // don't use "if" due to spurious wakeups.
wait();
return buffer.remove();
}
}
Esto es lo que podría suceder potencialmente:
Un hilo de consumidor llama take()
y ve que el buffer.isEmpty()
.
Antes de que el hilo del consumidor continúe llamando wait()
, aparece un hilo del productor e invoca un mensaje completo give()
, es decir,buffer.add(data); notify();
El hilo del consumidor ahora llamará wait()
(y perderá el notify()
que se acaba de llamar).
Si no tiene suerte, el hilo productor no producirá más give()
como resultado del hecho de que el hilo consumidor nunca se despierta, y tenemos un punto muerto.
Una vez que comprenda el problema, la solución es obvia: use synchronized
para asegurarse de que notify
nunca se llame entre isEmpty
y wait
.
Sin entrar en detalles: este problema de sincronización es universal. Como señala Michael Borgwardt, esperar / notificar se trata de comunicación entre subprocesos, por lo que siempre terminará con una condición de carrera similar a la descrita anteriormente. Esta es la razón por la cual se aplica la regla "solo esperar dentro de sincronizado".
Un párrafo del enlace publicado por @Willie lo resume bastante bien:
Necesita una garantía absoluta de que el camarero y el notificador estén de acuerdo sobre el estado del predicado. El camarero verifica el estado del predicado en algún momento un poco ANTES de que se duerma, pero depende de la corrección si el predicado es verdadero CUANDO se va a dormir. Hay un período de vulnerabilidad entre esos dos eventos, que puede romper el programa.
El predicado que el productor y el consumidor deben acordar está en el ejemplo anterior buffer.isEmpty()
. Y el acuerdo se resuelve asegurando que la espera y la notificación se realicen en synchronized
bloques.
Esta publicación ha sido reescrita como un artículo aquí: Java: ¿Por qué esperar debe ser llamado en un bloque sincronizado?