2009-06-24 4 views
42

He leído que siempre debemos llamado wait() desde el interior de un bucle:Por qué debe esperar() siempre se llamará dentro de un bucle

while (!condition) { obj.wait(); } 

Funciona bien sin un lazo ¿por qué es eso?

+2

Nota: la documentación de Oracle llama a este idioma un "bloque guardado" https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html –

Respuesta

7

Puede haber más de un trabajador esperando a que se cumpla una condición.

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.

5

Debido a esperar y Notify son utilizados para implementar [variables de condición] (http://en.wikipedia.org/wiki/Monitor_(synchronization)#Blocking_condition_variables) y por lo que necesita para comprobar si el predicado específico que está esperando en cierto antes de continuar.

+0

kd304 es correcto, no es solo que una condición no puede se han cumplido - es el hecho de que un hilo puede despertar espurosamente de una espera –

+0

@oxbow_lakes En lo que respecta al hilo que espera en una condición, no hay una diferencia real entre un despertar espurio y una notificación destinada a señal una condición diferente. De cualquier manera, debes verificar tu predicado. –

56

Es necesario no sólo para él, pero bucle revise su condición en el bucle. Java no garantiza que su flujo sea activado solo mediante una llamada notify()/notifyAll() o la llamada notificada()/notifyAll() correcta. Debido a esta propiedad, el bucle- menos versión podría funcionar en su entorno de desarrollo y fallar inesperadamente en el entorno de producción.

Por ejemplo, está esperando algo:

synchronized (theObjectYouAreWaitingOn) { 
    while (!carryOn) { 
     theObjectYouAreWaitingOn.wait(); 
    } 
} 

Un hilo mal viene y:

theObjectYouAreWaitingOn.notifyAll(); 

Si el hilo mal no hace/no puede meterse con el carryOn que acaba de seguir esperando para el cliente adecuado.

Editar: Se han añadido algunas muestras más. La espera se puede interrumpir. 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.

+4

Esta es la respuesta correcta. La documentación de espera: http://java.sun.com/javase/6/docs/api/java/lang/Object.html#wait(long) ... en realidad describe por qué necesita ponerlo en una loop - wakeup espurios. ¿Lo leyó el OP? –

+0

Ah, 'despertadores espurios'. No recuerdo el nombre. – akarnokd

+1

Debe actualizar su ejemplo de código para manejar ThreadInterruptedException en su espera. Entonces sería consistente con tu respuesta. – Robin

31

Es contestadas en documentation for Object.wait(long milis)

Un hilo también puede despertar sin haber sido notificada, interrumpida, o el tiempo de espera, la llamada de 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, espera siempre deben ocurrir en bucles, como éste:

synchronized (obj) { 
    while (<condition does not hold>) 
     obj.wait(timeout); 
    ... // Perform action appropriate to condition 
} 

(Para obtener más información sobre este tema, ver la Sección 3.2.3 en "Programación Concurrente de Doug Lea en Java (Segunda Edición)"(Addison-Wesley, 2000), o el artículo 50 en Joshua Bloch "Effective Java Programming Language Guía"(Addison-Wesley, 2001).

7

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 el queue para agregarlo, el consumidor n. ° 1 puede estar en la lectura de cerradura synchronized para entrar mientras el consumidor n. ° 2 ya está esperando. Cuando el elemento se agrega a la cola y se llama al notify, se notifica al # 2 y se mueve a la cola de espera, esperando el bloqueo queue, pero es detrás del el consumidor n. ° 1 que ya estaba esperando. Esto significa que el consumidor n. ° 1 puede avanzar primero para llamar al remove del queue. Si el bucle while es solo un if, cuando el consumidor n.º 2 recibe el candado y llama al remove, se producirá una excepción porque el queue estaba vacío. Solo porque fue notificado, necesita asegurarse de que el queue aún esté vacío debido a esta condición de carrera.

Esto está bien documentado. Aquí hay una página web que creé hace un tiempo que explica el race condition in detail y tiene un código de muestra.

+1

¿No debería ser 'while (queue.isEmpty())' sin '!'? – user104309

+0

Sí, tienes razón. Fijo. Gracias. – Gray

+0

cómo se puede considerar que el consumidor 1 se retira de la cola a menos que el consumidor 2 haya salido del bloque sincronizado. notificar de todos modos va a notificar al consumidor 2 y el consumidor 1 permanecerá inactivo. –

1

de su pregunta:

He leído que siempre debemos llamada de una espera() desde dentro de un bucle:

Aunque wait() normalmente espera hasta que notificar() o notifyAll () se llama, existe la posibilidad de que en casos muy raros el hilo de espera se pueda activar debido a un despertar espurio. 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 compruebe la condición en la que el hilo está esperando.

+0

Esta respuesta es incorrecta. No es el riesgo de despertares espurios por lo que 'wait' debe llamarse en un bucle. Es porque algún otro hilo puede haber manejado la condición. –

2

Tanto la seguridad como la vitalidad son inquietudes cuando se utiliza el mecanismo de espera/notificación. 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 de ciclo while 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 espera() está destinado a bloquear de forma indefinida hasta que se reciba una notificación, que aún debe ser encerrado dentro de un bucle para evitar que las siguientes vulnerabilidades:

hilo en el medio: Un tercer hilo puede adquirir el bloqueo en la compartida objeto 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 recibir una señal notifyAll() no se especifica. 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.

Despiertos espurios: Algunas 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 manera 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, la interfaz Condición

Cuando la espera de una condición, un "despertar espuria" se permite que ocurran , en general, como una concesión a la plataforma subyacente semántica. 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 bucle, probando el predicado de estado que se espera. Una implementación de 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 Código Noncompliant Este ejemplo de código que no cumple invoca el método wait() dentro de un bloque tradicional si falla y para comprobar la condición posterior después de que se reciba 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 
} 

Cumple Solución Esta solución compatible llama al método wait() desde dentro de un bucle while para comprobar el estado antes y después de la llamada a wait():

synchronized (object) { 
    while (<condition does not hold>) { 
    object.wait(); 
    } 
    // Proceed when condition holds 
} 

Las invocaciones de el método java.util.concurrent.locks.Condition.await() también debe estar encerrado en un bucle similar.

5

Creo que recibí la respuesta de @Gray.

Déjame intentar reformular eso para los novatos como yo y solicitar a los expertos que me corrijan si me equivoco.

Consumer bloque sincronizado::

synchronized (queue) { 
    // this needs to be while 
    while (queue.isEmpty()) { 
     queue.wait(); 
    } 
    queue.remove(); 
} 

Productor bloque sincronizado::

synchronized(queue) { 
// producer produces inside the queue 
    queue.notify(); 
} 

Supongamos ocurre lo siguiente en el orden dado:

1) consumidor # 2 se mete dentro del bloque de consumidor synchronized y está esperando sinc La cola está vacía.

2) Ahora, el productor obtiene el bloqueo en queue y se inserta dentro de la cola y llama a notify().

Ahora, ya sea consumidor # 1 puede ser elegido para funcionar que se espera de queue cerradura para entrar en el bloque de synchronized por primera vez

o

consumidor # 2 puede ser elegido para funcionar.

3) decir, el consumidor n. ° 1 se elige para continuar con la ejecución. Cuando comprueba la condición, será verdadera y se 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 al remove(), lo que podría ocasionar un comportamiento de excepción/inesperado.

Cuestiones relacionadas