sincronizados - synchronizable java
¿Por qué debe esperar() estar siempre en bloque sincronizado? (8)
¿Cuál es el daño potencial si fuera posible invocar a
wait()
fuera de un bloque sincronizado, conservando su semántica, suspendiendo el subproceso de la persona que llama?
Ilustremos 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 implementáramos una cola de bloqueo (lo sé, ya hay una en la API :)
Un primer intento (sin sincronización) podría mirar algo como las siguientes líneas
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 potencialmente podría pasar:
Un hilo de consumidor llama a
take()
y ve quebuffer.isEmpty()
.Antes de que el subproceso del consumidor pase a la llamada en
wait()
, aparece un subproceso productor e invoca ungive()
completo, es decir,buffer.add(data); notify();
buffer.add(data); notify();
El hilo del consumidor ahora llamará a
wait()
(y se perderá lanotify()
que se acaba de llamar).Si no tiene suerte, el subproceso del productor no producirá más
give()
como resultado del hecho de que el subproceso del consumidor nunca se despierta, y tenemos un punto muerto.
Una vez que comprenda el problema, la solución es obvia: siempre realice " 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 tiene que ver con la comunicación entre hilos, 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 coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/… resume bastante bien:
Necesita una garantía absoluta de que el camarero y el notificador están de acuerdo con el estado del predicado. El camarero verifica el estado del predicado en algún punto ligeramente ANTES de que se ponga en reposo, pero depende de que el predicado sea correcto CUANDO se apague. 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 bloques synchronized
.
Esta publicación se ha reescrito como un artículo aquí: Java: ¿Por qué debe llamarse esperar en un 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 al sincronizar un bloque en particular y luego liberar el monitor llamando a wait()
?
¿Cuál es el daño potencial si fuera posible invocar a wait()
fuera de un bloque sincronizado, conservando su semántica, suspendiendo el subproceso de la persona que llama?
@Rollerball tiene razón. Se llama a wait()
, de modo que el subproceso puede esperar a que ocurra alguna condición cuando ocurre esta llamada wait()
, el subproceso se ve obligado a abandonar su bloqueo.
Para renunciar a algo, primero debes tenerlo. El hilo debe poseer el bloqueo primero. 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 / bloque synchronized
. Sin embargo, como @ shrini1000 ha señalado, solo llamar a wait()
dentro del bloque sincronizado no evitará que ocurra esta inconsistencia.
Básicamente, esto tiene que ver con la arquitectura 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 a que el monitor lo ingrese. Además, cuando, por ejemplo, accediendo a una matriz sin un bloque sincronizado, es posible que otra hebra no vea el cambio en ella ... en realidad otra hebra no verá ningún cambio en ella cuando ya tenga una copia de la matriz en el caché de nivel x ( también conocido como cachés de 1º / 2º / 3º 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, porque tiene una copia propia del Objeto en su caché. Escribí sobre estos problemas aquí: https://.com/a/21462631 y Cuando un bloqueo contiene un objeto no final, ¿la referencia del objeto aún puede 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 las cosas de bajo nivel, como el funcionamiento de la CPU o la forma en que 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 con la jerarquía de memoria y la arquitectura de la CPU en primer lugar. "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 busca java y notifica un método de espera en particular?
Java solo mira el bloque de código sincronizado que estaba bloqueado por el objeto t. java no puede buscar todo el código para notificar a un t.wait () particular.
El problema que puede causar si no sincroniza antes de wait()
es el siguiente:
- Si el primer subproceso entra en
makeChangeOnX()
y comprueba la condición while, y estrue
(x.metCondition()
devuelvefalse
, significa quex.condition
esfalse
), por lo que entrará en él. Luego, justo antes del métodowait()
, otro subproceso va asetConditionToTrue()
y establece lax.condition
entrue
ynotifyAll()
. - Luego, solo después de eso, el primer subproceso entrará en su método de
wait()
(no afectado por lanotifyAll()
que ocurrió unos momentos antes). En este caso, el primer subproceso permanecerá a la espera de que otro subproceso realicesetConditionToTrue()
, pero puede que eso no vuelva a suceder.
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 de esperar (), notificar () y notificarAll () se utilizan para las comunicaciones entre subprocesos. Para deshacerse de la señal perdida y los problemas de activación falsos, el hilo de espera siempre espera en algunas condiciones. p.ej-
boolean wasNotified = false;
while(!wasNotified) {
wait();
}
Luego, notificar los conjuntos de subprocesos era la variable Notada notada a verdadero y notificar
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 descargaría en la memoria principal y estaría allí en el caché local del hilo, por lo que el hilo en espera seguirá esperando la señal, aunque se restableció notificando el hilo.
Para solucionar este tipo de problemas, estos métodos siempre se invocan dentro del bloque sincronizado, lo que garantiza que cuando se inicie el bloque sincronizado, todo se lea de la memoria principal y se vacíe en la memoria principal antes de salir del bloque sincronizado.
synchronized(monitor) {
boolean wasNotified = false;
while(!wasNotified) {
wait();
}
}
Gracias, espero que se aclare.
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 requiere sincronización para funcionar correctamente. Se podría argumentar que esto debería ser implícito, pero eso no ayudaría, por la siguiente razón:
Semánticamente, nunca wait()
. Necesitas alguna condición para ser satsificado, y si no es así, esperas hasta que lo sea. Entonces lo que realmente haces es
if(!condition){
wait();
}
Pero la condición se establece mediante un subproceso independiente, por lo que para que esto funcione correctamente, necesita sincronización.
Un par de cosas más están mal, porque solo porque el hilo que dejó de esperar no significa que la condición que está buscando es verdadera:
Puede obtener activaciones falsas (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 hilo hace que la condición sea falsa nuevamente en el momento en que el hilo en espera se activa (y vuelve a adquirir el monitor).
Para hacer frente a estos casos, lo que realmente necesita es siempre una variación de esto:
synchronized(lock){
while(!condition){
lock.wait();
}
}
Mejor aún, no se meta con las primitivas de sincronización y trabaje con las abstracciones ofrecidas en los paquetes java.util.concurrent
.
directamente desde this tutorial de Java oracle:
Cuando un hilo invoca d.wait, debe poseer el bloqueo intrínseco para d; de lo contrario, se produce un error. Invocar la espera dentro de un método sincronizado es una forma sencilla de adquirir el bloqueo intrínseco.