Por qué debe esperar () siempre estar en bloque sincronizado

Todos sabemos que para invocar Object.wait() , esta llamada debe colocarse en un bloque sincronizado, de lo contrario se IllegalMonitorStateException una IllegalMonitorStateException . ¿ Pero cuál es la razón para hacer esta restricción? Sé que wait() libera el monitor, pero ¿por qué necesitamos adquirir explícitamente el monitor haciendo que un bloque particular se sincronice y luego lo liberamos al llamar a wait() ?

¿Cuál es el daño potencial si fuera posible invocar wait() fuera de un bloque sincronizado, reteniendo su semántica, suspendiendo el hilo de la persona que llama?

Una wait() solo tiene sentido cuando también hay una notify() , por lo que siempre se trata de la comunicación entre subprocesos, y eso necesita que la sincronización funcione correctamente. Se podría argumentar que esto debería ser implícito, pero eso en realidad no ayudaría, por la siguiente razón:

Semánticamente, nunca wait() . Necesitas alguna condición para ser satsificado, y si no lo es, esperas hasta que lo esté. Entonces, lo que realmente haces es

 if(!condition){ wait(); } 

Pero la condición está siendo configurada por un hilo separado, por lo que para que esto funcione correctamente necesita sincronización.

Un par de cosas más están mal con eso, donde el hecho de que su hilo deje de esperar no significa que la condición que está buscando sea verdadera:

  • Puede obtener activaciones espúreas (lo que significa que un hilo puede despertar sin esperar recibir una notificación), o

  • La condición puede establecerse, pero un tercer hilo vuelve a hacer que la condición sea falsa cuando se despierta el hilo de espera (y vuelve a adquirir el monitor).

Para tratar con estos casos, lo que realmente necesita es siempre una variación de esto:

 synchronized(lock){ while(!condition){ lock.wait(); } } 

Mejor aún, no te metas con las primitivas de sincronización y trabajas con las abstracciones ofrecidas en los paquetes java.util.concurrent .

¿Cuál es el daño potencial si fuera posible invocar wait() fuera de un bloque sincronizado, reteniendo su semántica, suspendiendo el hilo de la persona que llama?

Vamos a ilustrar los problemas con los que nos encontraríamos si se pudiera llamar a wait() fuera de un bloque sincronizado con un ejemplo concreto .

Supongamos que implementamos una cola de locking (lo sé, ya hay una en la API 🙂

Un primer bash (sin sincronización) podría parecer algo en las líneas siguientes

 class BlockingQueue { Queue buffer = new LinkedList(); 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:

  1. Un hilo de consumidor llama a take() y ve que el buffer.isEmpty() .

  2. Antes de que el hilo consumidor llame a wait() , aparece un hilo productor e invoca un give() completo, es decir, buffer.add(data); notify(); buffer.add(data); notify();

  3. El hilo del consumidor ahora llamará a wait() (y omitirá el notify() que acaba de llamar).

  4. Si tiene mala suerte, el hilo del productor no producirá más give() como resultado del hecho de que el hilo del consumidor nunca se despierta, y tenemos un dead-lock.

Una vez que comprenda el problema, la solución es obvia: realice siempre give / isEmpty e isEmpty / wait atómicamente.

Sin entrar en detalles: este problema de sincronización es universal. Como señala Michael Borgwardt, esperar / notificar se trata de comunicación entre hilos, por lo que siempre terminarás con una condición de carrera similar a la descrita anteriormente. Esta es la razón por la cual se aplica la regla de “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 concuerden sobre el estado del predicado. El camarero comprueba el estado del predicado en algún momento ligeramente ANTES de que se ponga en modo de suspensión, pero depende de que el predicado sea correcto CUANDO se va a dormir. Hay un período de vulnerabilidad entre esos dos eventos, que puede romper el progtwig.

El predicado en el que el productor y el consumidor deben ponerse de acuerdo es en el ejemplo anterior buffer.isEmpty() . Y el acuerdo se resuelve al garantizar que la espera y la notificación se realicen en bloques synchronized .


Esta publicación se ha reescrito como un artículo aquí: Java: ¿Por qué esperar se debe llamar en un bloque sincronizado

@Rollerball tiene razón. Se wait() , de forma que el hilo puede esperar a que ocurra alguna condición cuando ocurre esta llamada a wait() , el hilo es forzado a abandonar su locking.
Para renunciar a algo, primero debe poseerlo. El hilo debe ser dueño primero del candado. De ahí la necesidad de llamarlo dentro de un método / bloque synchronized .

Sí, estoy de acuerdo con todas las respuestas anteriores con respecto a los posibles daños / inconsistencias si no verificó la condición dentro del método / locking synchronized . Sin embargo, como @ shrini1000 ha señalado, simplemente llamar a wait() dentro del bloque sincronizado no evitará que esta incoherencia ocurra.

Aquí hay una buena lectura …

El problema que puede causar si no sincroniza antes de wait() es el siguiente:

  1. Si el primer hilo va a makeChangeOnX() y verifica la condición while, y es true ( x.metCondition() devuelve false , significa que x.condition es false ) por lo que entrará en él. Luego, justo antes del método wait() , otro hilo va a setConditionToTrue() y establece x.condition en true y notifyAll() .
  2. Luego, solo después de eso, el primer hilo ingresará a su método wait() (no afectado por notifyAll() que sucedió unos momentos antes). En este caso, el primer hilo se mantendrá a la espera de que otro hilo ejecute setConditionToTrue() , pero puede que eso no vuelva a ocurrir.

Pero si pone synchronized antes de los métodos que cambian el estado del objeto, esto no sucederá.

 class A { private Object X; makeChangeOnX(){ while (! x.getCondition()){ wait(); } // Do the change } setConditionToTrue(){ x.condition = true; notifyAll(); } setConditionToFalse(){ x.condition = false; notifyAll(); } bool getCondition(){ return x.condition; } } 

Todos sabemos que los métodos wait (), notify () y notifyAll () se usan para comunicaciones entre subprocesos. Para deshacerse de la señal perdida y los problemas espurios de activación, el hilo de espera siempre espera en algunas condiciones. p.ej-

 boolean wasNotified = false; while(!wasNotified) { wait(); } 

Luego, la notificación de los conjuntos de hilos wasNotified variable a true y notify.

Cada hilo tiene su caché local, por lo que todos los cambios primero se escriben allí y luego se promueven gradualmente a la memoria principal.

Si estos métodos no se hubieran invocado dentro del bloque sincronizado, la variable wasNotified no se enjuagaría en la memoria principal y estaría allí en la memoria caché local del hilo para que el hilo en espera siga esperando la señal, aunque se restableció al notificar el hilo.

Para solucionar este tipo de problemas, estos métodos siempre se invocan dentro del bloque sincronizado, lo que asegura que cuando se inicia el locking sincronizado, todo se leerá desde la memoria principal y se descargará a la memoria principal antes de salir del bloque sincronizado.

 synchronized(monitor) { boolean wasNotified = false; while(!wasNotified) { wait(); } } 

Gracias, espero que aclare.

directamente de este tutorial de java oracle:

Cuando un hilo invoca d.wait, debe poseer el locking intrínseco para d; de lo contrario, se produce un error. Invocar wait dentro de un método sincronizado es una forma simple de adquirir el locking intrínseco.

Esto básicamente tiene que ver con la architecture del hardware (es decir, RAM y cachés ).

Si no usa synchronized junto con wait() o notify() , otro hilo podría ingresar al mismo bloque en lugar de esperar que el monitor lo ingrese. Además, cuando, por ejemplo, accede a una matriz sin un bloque sincronizado, es posible que otro subproceso no vea el cambio en ella … en realidad, otro subproceso no verá ningún cambio cuando ya tenga una copia de la matriz en la memoria caché de nivel x ( alias cachés de 1er / 2º / 3er nivel) del núcleo de CPU de manejo de subprocesos.

Pero los bloques sincronizados son solo un lado de la medalla: si realmente accede a un objeto dentro de un contexto sincronizado desde un contexto no sincronizado, el objeto aún no se sincronizará incluso dentro de un bloque sincronizado, ya que contiene una copia propia del objeto en su caché. Escribí sobre estos problemas aquí: https://stackoverflow.com/a/21462631 y Cuando un locking contiene un objeto no final, ¿puede la referencia del objeto seguir siendo modificada por otro hilo?

Además, estoy convencido de que los cachés de nivel x son responsables de la mayoría de los errores de tiempo de ejecución no reproducibles. Esto se debe a que los desarrolladores generalmente no aprenden cosas de bajo nivel, como el funcionamiento de la CPU o cómo la jerarquía de la memoria afecta la ejecución de las aplicaciones: http://en.wikipedia.org/wiki/Memory_hierarchy

Sigue siendo un acertijo por qué las clases de progtwigción no comienzan primero con la jerarquía de memoria y la architecture de la CPU. “Hola mundo” no ayudará aquí. 😉

Cuando llama a notify () desde un objeto t, java notifica un método t.wait () en particular. Pero, ¿cómo java busca y notifica un método de espera en particular?

java solo mira dentro del bloque sincronizado de código que fue bloqueado por el objeto t. Java no puede buscar todo el código para notificar a un t.wait () particular.