Lectura de un valor obsoleto después de leer un valor más nuevo [duplicado]

Nov 24 2020

Considere este ejemplo. Estaban teniendo:

int var = 0;

Hilo A:

System.out.println(var);
System.out.println(var);

Hilo B:

var = 1;

Los subprocesos se ejecutan al mismo tiempo. ¿Es posible la siguiente salida?

1
0

Es decir, el valor original se lee después de leer el nuevo valor. No vares volátil. Mi instinto es que no es posible.

Respuestas

3 Eugene Nov 26 2020 at 11:41

Está utilizando System.out.printlnque internamente hace synchronized(this) {...}que las cosas empeoren un poco más. Pero incluso con eso, el hilo de su lector aún puede observar 1, 0, es decir: una lectura picante.

No soy ni mucho menos un experto en esto, pero después de leer muchos videos / ejemplos / blogs de Alexey Shipilev, creo que entiendo al menos algo.

JLS afirma que:

Si xey son acciones del mismo hilo y x viene antes de y en el orden del programa, entonces hb (x, y).

Dado que ambas lecturas de varestán adentro program order, podemos dibujar:

                (po) 
firstRead(var) ------> secondRead(var)
// po == program order

Esa oración también dice que esto crea un happens-beforeorden, entonces:

                (hb) 
firstRead(var) ------> secondRead(var)
// hb == happens before

Pero eso está dentro de "el mismo hilo". Si queremos razonar sobre varios subprocesos, debemos analizar el orden de sincronización . Necesitamos eso porque el mismo párrafo sobre happens-before orderdice:

Si una acción x se sincroniza con una acción siguiente y, entonces también tenemos hb (x, y).

Entonces, si construimos esta cadena de acciones entre program ordery synchronizes-with order, podemos razonar sobre el resultado. Apliquemos eso a su código:

            (NO SW)                    (hb)
write(var) ---------> firstRead(var) -------> secondRead(var)

// NO SW == there is "no synchronizes-with order" here
// hb    == happens-before

Y aquí es donde happens-before consistencyentra en juego en el mismo capítulo :

Un conjunto de acciones A ocurre antes de ser consistente si para todas las lecturas r en A, donde W (r) es la acción de escritura vista por r, no es el caso que hb (r, W (r)) o que no haya existe una escritura w en A tal que wv = rv y hb (W (r), w) y hb (w, r).

En un conjunto consistente de acciones de ocurre antes de, cada lectura ve una escritura que se le permite ver mediante el pedido de ocurre antes de

Admito que entiendo muy vagamente la primera frase y aquí es donde Alexey más me ha ayudado, como él dice:

Lee ver la última escritura que ocurrió en la happens-beforeo cualquier otra escritura .

Debido a que no hay synchronizes-with orderallí, e implícitamente no hay happens-before order, el hilo de lectura puede leer a través de una carrera. y así obtener 1, que 0.


Tan pronto como introduzca uno correcto synchronizes-with order, por ejemplo uno de aquí

Una acción de desbloqueo en el monitor m se sincroniza con todas las acciones de bloqueo posteriores en ...

Una escritura en una variable volátil v se sincroniza con todas las lecturas posteriores de v por cualquier hilo ...

El gráfico cambia (digamos que eligió hacer var volatile):

               SW                       PO
write(var) ---------> firstRead(var) -------> secondRead(var)

// SW == there IS "synchronizes-with order" here
// PO == happens-before

PO(orden del programa) da eso HB(sucede antes) a través de la primera oración que cité en esta respuesta de JLS. Y SWda HBporque:

Si una acción x se sincroniza con una acción siguiente y, entonces también tenemos hb (x, y).

Como tal:

               HB                       HB
write(var) ---------> firstRead(var) -------> secondRead(var)

Y ahora happens-before orderdice que el hilo de lectura leerá el valor que estaba "escrito en el último HB", o significa que leer 1entonces 0es imposible.


Tomé las muestras de ejemplo de jcstress e introduje un pequeño cambio (como lo System.out.printlnhace el suyo ):

@JCStressTest
@Outcome(id = "0, 0", expect = Expect.ACCEPTABLE, desc = "Doing both reads early.")
@Outcome(id = "1, 1", expect = Expect.ACCEPTABLE, desc = "Doing both reads late.")
@Outcome(id = "0, 1", expect = Expect.ACCEPTABLE, desc = "Doing first read early, not surprising.")
@Outcome(id = "1, 0", expect = Expect.ACCEPTABLE_INTERESTING, desc = "First read seen racy value early, and the second one did not.")
@State
public class SO64983578 {

    private final Holder h1 = new Holder();
    private final Holder h2 = h1;

    private static class Holder {

        int a;
        int trap;
    }

    @Actor
    public void actor1() {
        h1.a = 1;
    }

    @Actor
    public void actor2(II_Result r) {
        Holder h1 = this.h1;
        Holder h2 = this.h2;
        
        h1.trap = 0;
        h2.trap = 0;

        synchronized (this) {
            r.r1 = h1.a;
        }

        synchronized (this) {
            r.r2 = h2.a;
        }

    }

}

Observe synchronized(this){....}que eso no es parte del ejemplo inicial. Incluso con la sincronización, todavía puedo ver eso 1, 0como resultado. Esto es solo para demostrar que incluso con synchronized(que proviene internamente de System.out.println), aún puede obtener 1que 0.

1 akuzminykh Nov 24 2020 at 17:04

Cuando varse lee el valor de y 1no volverá a cambiar. Esta salida no puede suceder, ni debido a la visibilidad ni a los reordenamientos. Lo que puede ocurrir es 0 0, 0 1y 1 1.

El punto clave para entender aquí es que printlninvolucra la sincronización. Mire dentro de ese método y debería ver un archivo synchronizedallí. Estos bloques tienen el efecto de que las impresiones se producirán en ese orden. Si bien la escritura puede ocurrir en cualquier momento, no es posible que la primera impresión vea el nuevo valor de varpero la segunda impresión vea el valor anterior. Por lo tanto, la escritura solo puede ocurrir antes de ambas impresiones, entre ellas o después de ellas.

Además de eso, no hay garantía de que la escritura sea visible en absoluto, ya varque no está marcada con volatileni la escritura está sincronizada de ninguna manera.

AlexRevetchi Nov 26 2020 at 15:32

Creo que lo que falta aquí es el hecho de que esos subprocesos se ejecutan en núcleos físicos reales y tenemos pocas variantes posibles aquí:

  1. todos los subprocesos se ejecutan en el mismo núcleo, luego el problema se reduce al orden de ejecución de esas 3 instrucciones, en este caso 1,0 no es posible, creo, las ejecuciones de println se ordenan debido a las barreras de memoria creadas por la sincronización, de modo que excluye 1,0

  2. A y B se ejecutan en 2 núcleos diferentes, entonces 1,0 tampoco parece posible, tan pronto como el núcleo que ejecuta el subproceso A lea 1, no hay forma de que lea 0 después, al igual que se ordenan las impresiones anteriores.

  3. El subproceso A se reprograma entre esos 2 printlns, por lo que el segundo println se ejecuta en un núcleo diferente, ya sea el mismo que se ejecutó / se ejecutará B o en un tercer núcleo diferente. Entonces, cuando los 2 printlns se ejecutan en diferentes núcleos, depende del valor que vean 2 núcleos, si var no está sincronizado (no está claro si var es miembro de esto), entonces esos 2 núcleos pueden ver diferentes valores de var, por lo que existe la posibilidad de 1,0.

Entonces este es un problema de coherencia de caché.

PD: No soy un experto en JVM, por lo que podría haber otras cosas en juego aquí.

FrancescoMenzani Nov 25 2020 at 01:35

Agregando a las otras respuestas:

Con longy double, las escrituras pueden no ser atómicas, por lo que los primeros 32 bits podrían volverse visibles antes que los últimos 32 bits, o viceversa. Por lo tanto, se podrían generar valores completamente diferentes.