java - hacer - Por qué debería esperar() siempre ser llamado dentro de un bucle
interfaz grafica java swing (8)
He leído que siempre debemos llamar a wait()
desde un bucle:
while (!condition) { obj.wait(); }
Funciona bien sin un bucle, ¿por qué es eso?
Creo que recibí la respuesta de @Gray.
Permítanme intentar reformular eso para los novatos como yo y pedirles a los expertos que me corrijan si me equivoco.
Bloque sincronizado del consumidor :
synchronized (queue) {
// this needs to be while
while (queue.isEmpty()) {
queue.wait();
}
queue.remove();
}
Bloque sincronizado de productor :
synchronized(queue) {
// producer produces inside the queue
queue.notify();
}
Suponga que sucede lo siguiente en el orden dado:
1) el consumidor n.º 2 ingresa al bloque synchronized
del consumidor y espera porque la cola está vacía.
2) Ahora, el productor obtiene el bloqueo en la queue
y se inserta dentro de la cola y llama a notify ().
Ahora, se puede elegir el consumidor n.º 1 para ejecutar, que está esperando que el bloqueo de queue
ingrese al bloque synchronized
por primera vez.
o
el consumidor n. ° 2 puede elegirse para ejecutarse.
3) decir, el consumidor n.º 1 se elige para continuar con la ejecución. Cuando comprueba la condición, será verdadera y remove()
de la cola.
4) decir, el consumidor n.º 2 procede de donde detuvo su ejecución (la línea después del método wait()
). Si la condición ''while'' no está allí (en su lugar una condición if
), simplemente procederá a llamar a remove()
que podría dar como resultado un comportamiento de excepción / inesperado.
De tu pregunta:
He leído que siempre debemos llamar a wait () desde un bucle:
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 activación espuria. En este caso, se reanuda un hilo de espera sin haber notificado a notify () o notifyAll ().
En esencia, el hilo se reanuda sin motivo aparente.
Debido a esta remota posibilidad, Oracle recomienda que las llamadas a wait () se realicen dentro de un bucle que verifique la condición en la que el hilo está esperando.
La razón principal por la cual los bucles while son tan importantes son las condiciones de carrera entre los hilos. Ciertamente, los despertadores espurios son reales y para ciertas arquitecturas, las condiciones comunes pero de carrera son una razón mucho más probable para el ciclo while.
Por ejemplo:
synchronized (queue) {
// this needs to be while
while (queue.isEmpty()) {
queue.wait();
}
queue.remove();
}
Con el código anterior, puede haber 2 hilos de consumo. Cuando el productor bloquea la queue
para agregarla, el consumidor n.º 1 puede estar en la lectura de bloqueo synchronized
para entrar mientras el consumidor n.º 2 ya está esperando. Cuando el artículo se agrega a la cola y se notify
llamó, se notifica al n. ° 2 y se mueve a la cola de espera, esperando el bloqueo de la queue
, pero está detrás del consumidor n. ° 1 que ya estaba esperando. Esto significa que el consumidor n. ° 1 puede avanzar primero para llamar a remove
de la queue
. Si el ciclo while es solo un if
, entonces cuando el consumidor n. ° 2 obtiene el bloqueo y las llamadas remove
, se produce una excepción porque la queue
estaba vacía. Solo porque se notificó, debe asegurarse de que la queue
aún esté vacía debido a esta condición de carrera.
Esto bien documentado. Aquí hay una página web que creé hace un tiempo atrás que explica la condición de carrera en detalle y tiene un código de muestra.
No solo necesita buclearlo sino verificar su condición en el ciclo. Java no garantiza que el hilo se active solo mediante una llamada notify () / notifyAll () o la notificación correcta () / notifyAll (). Debido a esta propiedad, la versión sin bucle podría funcionar en su entorno de desarrollo y fallar inesperadamente en el entorno de producción.
Por ejemplo, estás esperando algo:
synchronized (theObjectYouAreWaitingOn) {
while (!carryOn) {
theObjectYouAreWaitingOn.wait();
}
}
Un hilo malo viene y:
theObjectYouAreWaitingOn.notifyAll();
Si el hilo maligno no puede / no puede meterse con el carryOn
, simplemente continúa esperando al cliente adecuado.
Editar: se agregaron algunas muestras más. La espera puede ser interrumpida. Lanza InterruptedException y es posible que deba ajustar la espera en un try-catch. Dependiendo de las necesidades de su negocio, puede salir o suprimir la excepción y continuar esperando.
Porque wait y notify se usan para implementar [variables de condición] ( http://en.wikipedia.org/wiki/Monitor_(synchronization)#Blocking_condition_variables) y, por lo tanto, debe verificar si el predicado específico que está esperando es verdadero antes continuo.
Puede haber más de un trabajador esperando a que una condición se haga realidad.
Si dos o más trabajadores se despiertan (notificar a todos) tienen que verificar la condición nuevamente. de lo contrario, todos los trabajadores continuarían aunque solo haya datos para uno de ellos.
Se responde en la documentación de Object.wait (long milis)
Un hilo también puede despertarse sin que se le notifique, interrumpa o agote el tiempo, lo que se conoce como activación espuria. Si bien esto raramente ocurrirá en la práctica, las aplicaciones deben evitarlo al probar la condición que debería haber provocado el despertar del hilo y continuar esperando si la condición no se cumple. En otras palabras, las esperas siempre deben ocurrir en bucles, como este:
synchronized (obj) {
while (<condition does not hold>)
obj.wait(timeout);
... // Perform action appropriate to condition
}
(Para obtener más información sobre este tema, consulte la Sección 3.2.3 en la "Programación simultánea en Java (Segunda Edición)" de Doug Lea (Addison-Wesley, 2000), o el Artículo 50 en "Guía efectiva de lenguaje de programación en Java" de Joshua Bloch (Addison- Wesley, 2001).
Tanto la seguridad como la vitalidad son preocupaciones cuando se usa el mecanismo de esperar / notificar. La propiedad de seguridad requiere que todos los objetos mantengan estados consistentes en un entorno multiproceso. La propiedad Liveness requiere que cada operación o invocación de método se ejecute hasta su finalización sin interrupción.
Para garantizar la vitalidad, los programas deben probar la condición while loop antes de invocar el método wait (). Esta prueba temprana verifica si otro hilo ya ha satisfecho el predicado de condición y envió una notificación. Invocar el método wait () después de enviar la notificación produce un bloqueo indefinido.
Para garantizar la seguridad, los programas deben probar la condición while loop después de regresar del método wait (). Aunque wait () está destinado a bloquearse indefinidamente hasta que se recibe una notificación, aún debe estar dentro de un bucle para evitar las siguientes vulnerabilidades:
Subproceso en el medio: un tercer subproceso puede adquirir el bloqueo en el objeto compartido durante el intervalo entre una notificación que se envía y el hilo de recepción que reanuda la ejecución. Este tercer hilo puede cambiar el estado del objeto, dejándolo inconsistente. Esta es una condición de carrera de tiempo de verificación y tiempo de uso (TOCTOU).
Notificación maliciosa: se puede recibir una notificación aleatoria o maliciosa cuando el predicado de condición es falso. Tal notificación cancelaría el método wait ().
Notificación mal entregada: el orden en que se ejecutan los hilos después de la recepción de una señal notifyAll () no está especificado. En consecuencia, un hilo no relacionado podría comenzar a ejecutarse y descubrir que se cumple su predicado de condición. En consecuencia, podría reanudar la ejecución a pesar de que se le exige que permanezca inactivo.
Despertadores espurios: ciertas implementaciones de Java Virtual Machine (JVM) son vulnerables a las activaciones espúreas que provocan que los subprocesos de espera se activen incluso sin notificación.
Por estas razones, los programas deben verificar el predicado de condición después de que el método wait () regrese. Un ciclo while es la mejor opción para verificar el predicado de condición tanto antes como después de invocar wait ().
De forma similar, el método await () de la interfaz Condición también debe invocarse dentro de un bucle. De acuerdo con la API de Java, condición de la interfaz
Cuando se espera una Condición, se permite que ocurra una "activación espuria", en general, como una concesión a la semántica de la plataforma subyacente. Esto tiene poco impacto práctico en la mayoría de los programas de aplicación, ya que una Condición siempre se debe esperar en un ciclo, probando el predicado de estado que se está esperando. Una implementación es libre de eliminar la posibilidad de activaciones falsas, pero se recomienda que los programadores de aplicaciones siempre asuman que pueden ocurrir y, por lo tanto, siempre esperen en un bucle.
El nuevo código debe usar las utilidades de concurrencia java.util.concurrent.locks en lugar del mecanismo de espera / notificación. Sin embargo, se permite que el código heredado que cumpla con los otros requisitos de esta regla dependa del mecanismo de espera / notificación.
Ejemplo de código no conforme Este ejemplo de código no conforme invoca el método wait () dentro de un bloque if tradicional y falla al verificar la condición posterior después de recibir la notificación. Si la notificación fue accidental o maliciosa, el hilo podría despertarse prematuramente.
synchronized (object) {
if (<condition does not hold>) {
object.wait();
}
// Proceed when condition holds
}
Solución compatible Esta solución compatible llama al método wait () desde un ciclo while para verificar la condición antes y después de la llamada a wait ():
synchronized (object) {
while (<condition does not hold>) {
object.wait();
}
// Proceed when condition holds
}
Las invocaciones del método java.util.concurrent.locks.Condition.await () también se deben incluir en un bucle similar.