Uma operação sensível no meu laboratório hoje deu completamente errado. Um atuador em um microscópio eletrônico ultrapassou seus limites e, após uma cadeia de eventos, perdi US $ 12 milhões em equipamentos. Eu reduzi mais de 40 mil linhas no módulo defeituoso para isso:
import java.util.*;
class A {
static Point currentPos = new Point(1,2);
static class Point {
int x;
int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
public static void main(String[] args) {
new Thread() {
void f(Point p) {
synchronized(this) {}
if (p.x+1 != p.y) {
System.out.println(p.x+" "+p.y);
System.exit(1);
}
}
@Override
public void run() {
while (currentPos == null);
while (true)
f(currentPos);
}
}.start();
while (true)
currentPos = new Point(currentPos.x+1, currentPos.y+1);
}
}
Algumas amostras da saída que estou recebendo:
$ java A
145281 145282
$ java A
141373 141374
$ java A
49251 49252
$ java A
47007 47008
$ java A
47427 47428
$ java A
154800 154801
$ java A
34822 34823
$ java A
127271 127272
$ java A
63650 63651
Como não há aritmética de ponto flutuante aqui, e todos sabemos que números inteiros assinados se comportam bem no estouro de Java, acho que não há nada de errado com esse código. No entanto, apesar da saída indicar que o programa não atingiu a condição de saída, ele atingiu a condição de saída (foi atingido e não atingido?). Por quê?
Notei que isso não acontece em alguns ambientes. Estou no OpenJDK 6 no Linux de 64 bits.
final
qualificador (que não afeta o código de código produzido) aos camposx
ey
"resolver" o erro. Embora não afete o bytecode, os campos são sinalizados com ele, o que me leva a pensar que esse é um efeito colateral de uma otimização da JVM.Point
p
é construído que satisfazp.x+1 == p.y
, então uma referência é passada para o encadeamento de polling. Eventualmente, o encadeamento de polling decide sair porque acha que a condição não é atendida para um dosPoint
s que recebe, mas a saída do console mostra que deveria ter sido atendida. A faltavolatile
daqui simplesmente significa que o tópico de pesquisa pode ficar preso, mas esse claramente não é o problema aqui.synchronized
faz com que o erro não aconteça? Isso porque eu tive que escrever código aleatoriamente até encontrar um que reproduzisse esse comportamento deterministicamente.Respostas:
currentPos = new Point(currentPos.x+1, currentPos.y+1);
faz algumas coisas, incluindo a gravação de valores padrão emx
ey
(0) e a gravação de seus valores iniciais no construtor. Como seu objeto não é publicado com segurança, essas 4 operações de gravação podem ser reordenadas livremente pelo compilador / JVM.Portanto, da perspectiva do encadeamento de leitura, é uma execução legal ler
x
com seu novo valor, masy
com seu valor padrão 0, por exemplo. Quando você alcança aprintln
instrução (que por sinal é sincronizada e, portanto, influencia as operações de leitura), as variáveis têm seus valores iniciais e o programa imprime os valores esperados.Marcação
currentPos
comovolatile
garantirá uma publicação segura, pois seu objeto é efetivamente imutável - se, no seu caso de uso real, o objetovolatile
sofrer uma mutação após a construção, as garantias não serão suficientes e você poderá ver um objeto inconsistente novamente.Como alternativa, você pode tornar o
Point
imutável, o que também garantirá uma publicação segura, mesmo sem usarvolatile
. Para alcançar a imutabilidade, basta marcarx
ey
finalizar.Como uma observação lateral e, como já mencionado,
synchronized(this) {}
pode ser tratado como não operacional pela JVM (eu entendo que você o incluiu para reproduzir o comportamento).fonte
Como
currentPos
está sendo alterado fora do segmento, deve ser marcado comovolatile
:Sem volátil, não é garantido que o encadeamento leia as atualizações do currentPos que estão sendo feitas no encadeamento principal. Portanto, novos valores continuam sendo gravados para o currentPos, mas o encadeamento continua a usar as versões em cache anteriores por motivos de desempenho. Como apenas um thread modifica o currentPos, você pode fugir sem bloqueios, o que melhorará o desempenho.
Os resultados parecerão muito diferentes se você ler os valores apenas uma única vez no encadeamento para uso na comparação e exibição subsequente deles. Quando eu faço o seguinte
x
sempre é exibido como1
ey
varia entre0
e algum número inteiro grande. Eu acho que o comportamento dele neste momento é um pouco indefinido sem avolatile
palavra-chave e é possível que a compilação JIT do código esteja contribuindo para que ele atue dessa maneira. Além disso, se eu comentar osynchronized(this) {}
bloco vazio , o código funcionará bem e suspeito que seja porque o bloqueio causa atraso suficientecurrentPos
e que seus campos sejam relidos em vez de usados no cache.fonte
volatile
.Você tem memória comum, a referência 'currentpos' e o objeto Point e seus campos atrás dele, compartilhados entre 2 threads, sem sincronização. Portanto, não há uma ordem definida entre as gravações que ocorrem nessa memória no encadeamento principal e as leituras no encadeamento criado (chame-o de T).
O thread principal está fazendo as seguintes gravações (ignorando a configuração inicial do point, resultará em px e py com valores padrão):
Como não há nada de especial nessas gravações em termos de sincronização / barreiras, o tempo de execução é livre para permitir que o encadeamento T os veja ocorrerem em qualquer ordem (o encadeamento principal sempre vê gravações e leituras ordenadas de acordo com a ordem do programa) e ocorre a qualquer momento entre as leituras em T.
Então, T está fazendo:
Dado que não há relações de ordenação entre as gravações em main e as leituras em T, existem claramente várias maneiras de gerar esse resultado, pois T pode ver a gravação de main em currentpos antes das gravações em currentpos.y ou currentpos.x:
e assim por diante ... Há várias corridas de dados aqui.
Eu suspeito que a suposição defeituosa aqui está pensando que as gravações resultantes dessa linha são visíveis em todos os threads na ordem do programa que o executa:
Java não oferece essa garantia (seria terrível para o desempenho). Algo mais deve ser adicionado se o seu programa precisar de uma ordem garantida das gravações em relação às leituras em outros threads. Outros sugeriram que os campos x, y sejam finais ou, alternativamente, tornem os pos de corrente voláteis.
O uso final tem a vantagem de tornar os campos imutáveis e, assim, permitir que os valores sejam armazenados em cache. O uso de leads voláteis para sincronização em todas as gravações e leituras dos registros atuais, o que pode prejudicar o desempenho.
Consulte o capítulo 17 do Java Language Spec para obter detalhes sórdidos: http://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
(A resposta inicial assumiu um modelo de memória mais fraco, pois eu não tinha certeza de que o volátil garantido pelo JLS era suficiente. Resposta editada para refletir o comentário das assilias, apontando que o modelo Java é mais forte - acontece - antes é transitivo - e tão volátil no currentpos também é suficiente )
fonte
currentPos
for tornada volátil, a atribuição garante a publicação segura docurrentPos
objeto e de seus membros, mesmo que eles próprios não sejam voláteis.Point currentPos = new Point(x, y)
, você tem 3 gravações: (w1)this.x = x
, (w2)this.y = y
e (w3)currentPos = the new point
. A ordem do programa garante que hb (w1, w3) e hb (w2, w3). Mais tarde no programa, você lê (r1)currentPos
. SecurrentPos
não é volátil, não há hb entre r1 e w1, w2, w3; portanto, r1 pode observar qualquer um (ou nenhum) deles. Com volátil, você apresenta hb (w3, r1). E o relacionamento hb é transitivo, então você também introduz hb (w1, r1) e hb (w2, r1). Isso está resumido em Concorrência Java na Prática (3.5.3. Idiomas de Publicação Segura).Você pode usar um objeto para sincronizar as gravações e leituras. Caso contrário, como outros disseram anteriormente, uma gravação no currentPos ocorrerá no meio das duas leituras p.x + 1 e py
fonte
sem
não é compartilhada e tratar a instrução sincronizada como não operacional ... O fato de resolver o problema é pura sorte.Você está acessando currentPos duas vezes e não oferece garantia de que não seja atualizado entre esses dois acessos.
Por exemplo:
Você está basicamente comparando dois diferentes pontos .
Observe que mesmo tornar o currentPos volátil não o protegerá disso, pois são duas leituras separadas pelo thread de trabalho.
Adicione um
método para sua classe de pontos. Isso garantirá que apenas um valor de currentPos seja usado ao marcar x + 1 == y.
fonte