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


257

Todos sabemos que para invocar Object.wait(), esta llamada debe colocarse en un bloque sincronizado, de lo contrario IllegalMonitorStateExceptionse lanza un. 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 al sincronizar un bloque particular y luego liberar el monitor llamando wait()?

¿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?

Respuestas:


232

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

Semánticamente, nunca solo wait(). Necesitas alguna condición para ser satsificado, y si no es así, espera hasta que lo sea. Entonces, lo que realmente haces es

if(!condition){
    wait();
}

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

Un par de cosas más mal con él, donde solo porque su hilo dejó de esperar no significa que la condición que está buscando sea verdadera:

  • Puede obtener despertadores espurios (lo que significa que un hilo puede despertarse de la espera sin haber recibido una notificación), o

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

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

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

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


3
Aquí también hay una discusión detallada, que dice esencialmente lo mismo. coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/…

1
por cierto, si no debe ignorar la bandera interrumpida, el bucle también se comprobará Thread.interrupted().
bestsss

2
Todavía puedo hacer algo como: while (! Condition) {sincronizado (esto) {wait ();}} lo que significa que todavía hay una carrera entre verificar la condición y esperar incluso si wait () se llama correctamente en un bloque sincronizado. Entonces, ¿hay alguna otra razón detrás de esta restricción, tal vez debido a la forma en que se implementa en Java?
shrini1000

99
Otro escenario desagradable: la condición es falsa, estamos a punto de entrar en wait () y luego otro hilo cambia la condición e invoca notify (). Debido a que todavía no estamos en wait (), perderemos esta notificación (). En otras palabras, probar y esperar, así como cambiar y notificar, deben ser atómicos .

1
@Nullpointer: si se trata de un tipo que se puede escribir atómicamente (como el booleano implícito al usarlo directamente en una cláusula if) y no hay interdependencia con otros datos compartidos, podría salirse con la suposición de que es volátil. Pero necesita eso o sincronización para asegurarse de que la actualización sea visible para otros subprocesos rápidamente.
Michael Borgwardt

283

¿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:

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

  2. 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();

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

  4. 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 synchronizedpara asegurarse de que notifynunca se llame entre isEmptyy 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 synchronizedbloques.


Esta publicación ha sido reescrita como un artículo aquí: Java: ¿Por qué esperar debe ser llamado en un bloque sincronizado?


Además, para asegurarme de que los cambios realizados en la condición se vean inmediatamente después de que finalice el wait (), supongo. De lo contrario, también un punto muerto porque ya se ha llamado a notify ().
Surya Wijaya Madjid

Interesante, pero tenga en cuenta que simplemente llamar sincronizado en realidad no siempre resolverá tales problemas debido a la naturaleza "poco confiable" de wait () y notify (). Lea más aquí: stackoverflow.com/questions/21439355/… . La razón por la que se necesita sincronizado se encuentra dentro de la arquitectura de hardware (vea mi respuesta a continuación).
Marcus

pero si se agrega return buffer.remove();mientras bloquea pero después wait();, ¿funciona?
BobJiang

@BobJiang, no, el hilo se puede despertar por otras razones que no sea alguien llamando a dar. En otras palabras, el búfer puede estar vacío incluso después de waitretornos.
aioobe

Solo tengo Thread.currentThread().wait();en la mainfunción rodeado de try-catch para InterruptedException. Sin synchronizedbloqueo, me da la misma excepción IllegalMonitorStateException. ¿Qué lo hace llegar al estado ilegal ahora? Sin synchronizedembargo, funciona dentro del bloque.
Shashwat

12

@Rollerball tiene razón. Se wait()llama al, para que el hilo pueda esperar a que ocurra alguna condición cuando se produce esta wait()llamada, el hilo se ve obligado a abandonar su bloqueo.
Para renunciar a algo, primero debes tenerlo. El subproceso debe ser el propietario de la cerradura primero. De ahí la necesidad de llamarlo dentro de un synchronizedmétodo / bloque.

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 synchronizedmétodo / bloque. Sin embargo, como señaló @ shrini1000, solo llamar wait()dentro del bloque sincronizado no evitará que ocurra esta inconsistencia.

Aquí hay una buena lectura ...


55
@Popeye Explique 'correctamente' correctamente. Tu comentario no sirve para nadie.
Marqués de Lorne

4

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

  1. Si el primer subproceso entra makeChangeOnX()y comprueba la condición while, y está true( x.metCondition()devuelve false, significa que x.conditionestá false) para que pueda entrar. Luego, justo antes del wait()método, otro subproceso va a setConditionToTrue()y establece el x.conditiona truey notifyAll().
  2. Luego, solo después de eso, el primer hilo ingresará a su wait()método (no afectado por lo notifyAll()que sucedió unos momentos antes). En este caso, el primer subproceso permanecerá esperando que se ejecute otro subproceso setConditionToTrue(), pero es posible que eso no vuelva a suceder.

Pero si antepone synchronizedlos 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;
    }
}

2

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

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

Luego, notificar los conjuntos de hilos wasNotified variable a true y notificar.

Cada subproceso 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 invocan dentro del bloque sincronizado, la variable wasNotified no se enjuagaría en la memoria principal y estaría allí en la caché local del subproceso, por lo que el subproceso en espera seguirá esperando la señal aunque se restableció notificando el subproceso.

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

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

Gracias, espero que te aclare.


1

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

Si no lo usa synchronizedjunto con wait()o notify(), otro hilo podría ingresar al mismo bloque en lugar de esperar a que el monitor lo ingrese. Además, cuando, por ejemplo, acceda a una matriz sin un bloque sincronizado, es posible que otro subproceso no vea el cambio en él ... en realidad, otro subproceso no verá ningún cambio cuando ya tenga una copia de la matriz en el caché de nivel x ( también conocido como cachés de 1er / 2do / 3er nivel) del núcleo de la CPU que maneja el hilo.

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, porque contiene una copia propia del objeto en su caché. Escribí sobre estos problemas aquí: https://stackoverflow.com/a/21462631 y cuando un candado contiene un objeto no final, ¿puede la referencia del objeto ser cambiada 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 memoria afecta el funcionamiento de las aplicaciones: http://en.wikipedia.org/wiki/Memory_hierarchy

Sigue siendo un enigma por qué las clases de programación no comienzan primero con la jerarquía de memoria y la arquitectura de la CPU. "Hola mundo" no ayudará aquí. ;)


1
Acabo de descubrir un sitio web que lo explica perfectamente y en profundidad: javamex.com/tutorials/…
Marcus

Hmm ... no estoy seguro de seguir. Si el almacenamiento en caché fue la única razón para poner esperar y notificar dentro de sincronizado, ¿por qué no se pone la sincronización dentro de la implementación de esperar / notificar?
aioobe

Buena pregunta, ya que esperar / notificar podría muy bien ser métodos sincronizados ... ¿quizás los antiguos desarrolladores de Java de Sun saben la respuesta? Eche un vistazo al enlace de arriba, o tal vez esto también lo ayude: docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

Una razón podría ser: en los primeros días de Java no había errores de compilación cuando no se llamaba sincronizado antes de realizar estas operaciones de subprocesamiento múltiple. En cambio, solo hubo errores de tiempo de ejecución (por ejemplo, coderanch.com/t/239491/java-programmer-SCJP/certification/… ). Tal vez realmente pensaron @SUN que cuando los programadores reciben estos errores, se contactan con ellos, lo que puede haberles dado la oportunidad de vender más de sus servidores. ¿Cuándo cambió? Tal vez Java 5.0 o 6.0, pero en realidad no recuerdo ser sincero ...
Marcus

TBH Veo algunos problemas con su respuesta 1) Su segunda oración no tiene sentido: no importa en qué objeto un hilo tiene un bloqueo. Independientemente del objeto en el que se sincronizan dos hilos, todos los cambios se hacen visibles. 2) Dices que otro hilo "no" verá ningún cambio. Esto debería ser "no puede" . 3) No sé por qué está sacando cachés de primer / segundo / tercer nivel ... Lo que importa aquí es lo que dice el modelo de memoria Java y eso se especifica en JLS. Si bien la arquitectura de hardware puede ayudar a comprender por qué JLS dice lo que hace, es estrictamente irrelevante en este contexto.
aioobe

0

directamente de este tutorial de oráculo de Java:

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


De la pregunta que hizo el autor, no parece que el autor de la pregunta tenga una comprensión clara de lo que cité en el tutorial. Y, además, mi respuesta explica "Por qué".
Rollerball

0

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

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


0

según los documentos:

El hilo actual debe poseer el monitor de este objeto. El hilo libera la propiedad de este monitor.

wait()El método simplemente significa que libera el bloqueo del objeto. Por lo tanto, el objeto se bloqueará solo dentro del bloque / método sincronizado. Si el hilo está fuera del bloque de sincronización significa que no está bloqueado, si no está bloqueado, ¿qué liberarías en el objeto?


0

Subproceso de espera en el objeto de supervisión (objeto utilizado por el bloque de sincronización), puede haber un número n de objeto de supervisión en todo el viaje de un único subproceso. Si el subproceso espera fuera del bloque de sincronización, entonces no hay ningún objeto de supervisión y tampoco otro subproceso notifica para acceder al objeto de supervisión, entonces, ¿cómo sabría el subproceso fuera del bloque de sincronización que ha sido notificado? Esta es también una de las razones por las que wait (), notify () y notifyAll () están en la clase de objeto en lugar de la clase de subproceso.

Básicamente, el objeto de supervisión es un recurso común aquí para todos los subprocesos, y los objetos de supervisión solo pueden estar disponibles en el bloque de sincronización.

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.