Lendo um valor desatualizado depois que um valor mais recente foi lido [duplicado]

Nov 24 2020

Considere este exemplo. Estamos tendo:

int var = 0;

Tópico A:

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

Tópico B:

var = 1;

Os threads são executados simultaneamente. A seguinte saída é possível?

1
0

Ou seja, o valor original é lido após o novo valor ser lido. O varnão é volátil. Meu pressentimento é que não é possível.

Respostas

3 Eugene Nov 26 2020 at 11:41

Você está usando uma função System.out.printlninterna synchronized(this) {...}que tornará as coisas um pouco mais piores. Mas mesmo com isso, o thread do seu leitor ainda pode observar 1, 0, ou seja: uma leitura atrevida.

De longe não sou um especialista nisso, mas depois de passar por muitos vídeos / exemplos / blogs de Alexey Shipilev, acho que entendi pelo menos alguma coisa.

JLS afirma que:

Se xey são ações da mesma thread e x vem antes de y na ordem do programa, então hb (x, y).

Como ambas as leituras varestão disponíveis program order, podemos desenhar:

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

Essa frase também diz que isso cria um happens-beforepedido, então:

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

Mas isso está dentro do "mesmo segmento". Se quisermos raciocinar sobre vários threads, precisamos examinar a ordem de sincronização . Precisamos disso porque o mesmo parágrafo sobre happens-before orderdiz:

Se uma ação x sincroniza com uma ação seguinte y, então também temos hb (x, y).

Portanto, se construirmos essa cadeia de ações entre program ordere synchronizes-with order, podemos raciocinar sobre o resultado. Vamos aplicar isso ao seu código:

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

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

E é aqui que happens-before consistencyentra em jogo no mesmo capítulo :

Um conjunto de ações A ocorre antes de consistente se para todas as leituras r em A, onde W (r) é a ação de escrita vista por r, não é o caso de que hb (r, W (r)) ou que haja existe uma escrita w em A tal que wv = rv e hb (W (r), w) e hb (w, r).

Em um conjunto de ações acontece antes de acontecer, cada leitura vê uma gravação que pode ser vista por acontece antes de ordenar

Admito que entendo muito vagamente a primeira frase e foi aqui que Alexey mais me ajudou, como ele mesmo diz:

As leituras veem a última gravação ocorrida no happens-beforeou qualquer outra gravação .

Como não synchronizes-with orderexiste, e implicitamente não existe happens-before order, o thread de leitura pode ser lido por meio de uma corrida. e assim obter 1, do que 0.


Assim que você introduzir um correto synchronizes-with order, por exemplo um daqui

Uma ação de desbloqueio no monitor m sincroniza-se com todas as ações de bloqueio subsequentes em ...

Uma gravação em uma variável volátil v sincroniza com todas as leituras subsequentes de v por qualquer thread ...

O gráfico muda (digamos que você optou por fazer var volatile):

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

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

PO(ordem do programa) fornece isso HB(acontece antes) por meio da primeira frase que citei nesta resposta do JLS. E SWHBporque:

Se uma ação x sincroniza com uma ação seguinte y, então também temos hb (x, y).

Assim sendo:

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

E agora happens-before orderdiz que o thread de leitura irá ler o valor que foi "escrito no último HB", ou isso significa que a leitura 1então 0é impossível.


Peguei os exemplos do jcstress e introduzi uma pequena mudança (assim como você System.out.printlnfaz):

@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 isso não faz parte do exemplo inicial. Mesmo com a sincronização, ainda posso ver isso 1, 0como resultado. Isso é apenas para provar que mesmo com synchronized(que vem internamente de System.out.println), você ainda pode obter do 1que 0.

1 akuzminykh Nov 24 2020 at 17:04

Quando o valor de varé lido e não é 1alterado novamente. Essa saída não pode acontecer, nem por visibilidade nem por reordenamentos. O que pode acontecer é 0 0, 0 1e 1 1.

O ponto chave a entender aqui é que printlnenvolve sincronização. Olhe dentro desse método e você verá um synchronizedlá. Esses blocos têm o efeito de que as impressões acontecerão exatamente nessa ordem. Embora a gravação possa acontecer a qualquer momento, não é possível que a primeira impressão veja o novo valor de, varmas a segunda impressão veja o valor antigo. Portanto, a gravação só pode acontecer antes de ambas as impressões, no meio ou depois delas.

Além disso, não há garantia de que a gravação será visível de todo, pois varnão está marcada com volatilenem a gravação está sincronizada de forma alguma.

AlexRevetchi Nov 26 2020 at 15:32

Acho que o que está faltando aqui é o fato de que esses threads são executados em núcleos físicos reais e temos poucas variantes possíveis aqui:

  1. todos os threads rodam no mesmo núcleo, então o problema é reduzido à ordem de execução dessas 3 instruções, neste caso 1,0 não é possível, eu acho, as execuções de println são ordenadas devido às barreiras de memória criadas pela sincronização, para que exclui 1,0

  2. A e B são executados em 2 núcleos diferentes, então 1,0 também não parece possível, pois assim que o núcleo que executa o thread A ler 1, não há como ele ler 0 depois, da mesma forma que os printlns acima são ordenados.

  3. O thread A é reprogramado entre essas 2 printlns, então a segunda println é executada em um core diferente, seja o mesmo que B foi / será executado ou em um 3º core diferente. Portanto, quando os 2 printlns são executados em núcleos diferentes, depende do valor que os 2 núcleos veem, se var não estiver sincronizado (não está claro se var é membro disso), então esses 2 núcleos podem ver valores de var diferentes, então existe a possibilidade de 1,0.

Portanto, este é um problema de coerência do cache.

PS Não sou um especialista em jvm, então pode haver outras coisas em jogo aqui.

FrancescoMenzani Nov 25 2020 at 01:35

Somando-se às outras respostas:

Com longe double, as gravações não podem ser atômicas, portanto, os primeiros 32 bits podem se tornar visíveis antes dos últimos 32 bits, ou vice-versa. Portanto, valores completamente diferentes podem ser produzidos.