Lendo um valor desatualizado depois que um valor mais recente foi lido [duplicado]
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 var
não é volátil. Meu pressentimento é que não é possível.
Respostas
Você está usando uma função System.out.println
interna 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 var
estã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-before
pedido, 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 order
diz:
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 order
e 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 consistency
entra 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-before
ou qualquer outra gravação .
Como não synchronizes-with order
existe, 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 SW
dá HB
porque:
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 order
diz que o thread de leitura irá ler o valor que foi "escrito no último HB", ou isso significa que a leitura 1
então 0
é impossível.
Peguei os exemplos do jcstress e introduzi uma pequena mudança (assim como você System.out.println
faz):
@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, 0
como resultado. Isso é apenas para provar que mesmo com synchronized
(que vem internamente de System.out.println
), você ainda pode obter do 1
que 0
.
Quando o valor de var
é lido e não é 1
alterado novamente. Essa saída não pode acontecer, nem por visibilidade nem por reordenamentos. O que pode acontecer é 0 0
, 0 1
e 1 1
.
O ponto chave a entender aqui é que println
envolve sincronização. Olhe dentro desse método e você verá um synchronized
lá. 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, var
mas 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 var
não está marcada com volatile
nem a gravação está sincronizada de forma alguma.
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:
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
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.
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.
Somando-se às outras respostas:
Com long
e 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.