Eu sei que operações compostas como i++
não são seguras para threads, pois envolvem várias operações.
Mas verificar a referência em si é uma operação segura para threads?
a != a //is this thread-safe
Tentei programar isso e usar vários threads, mas não falhou. Acho que não consegui simular a corrida na minha máquina.
EDITAR:
public class TestThreadSafety {
private Object a = new Object();
public static void main(String[] args) {
final TestThreadSafety instance = new TestThreadSafety();
Thread testingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
long countOfIterations = 0L;
while(true){
boolean flag = instance.a != instance.a;
if(flag)
System.out.println(countOfIterations + ":" + flag);
countOfIterations++;
}
}
});
Thread updatingReferenceThread = new Thread(new Runnable() {
@Override
public void run() {
while(true){
instance.a = new Object();
}
}
});
testingReferenceThread.start();
updatingReferenceThread.start();
}
}
Este é o programa que estou usando para testar a segurança de threads.
Comportamento estranho
Quando meu programa inicia entre algumas iterações, recebo o valor do sinalizador de saída, o que significa que a !=
verificação de referência falha na mesma referência. MAS, após algumas iterações, a saída se torna um valor constante false
e, em seguida, a execução do programa por um longo período de tempo não gera uma única true
saída.
Como a saída sugere após algumas iterações n (não fixas), a saída parece ser um valor constante e não muda.
Resultado:
Para algumas iterações:
1494:true
1495:true
1496:true
19970:true
19972:true
19974:true
//after this there is not a single instance when the condition becomes true
fonte
1234:true
não esmagar o outro ). Um teste de corrida precisa de um loop interno mais apertado. Imprima um resumo no final (como alguém fez abaixo com uma estrutura de teste de unidade).Respostas:
Na ausência de sincronização, este código
pode produzir
true
. Este é o bytecode paratest()
como podemos ver, ele carrega o campo
a
para os vars locais duas vezes, é uma operação não atômica, se tivera
sido alterada no meio por outra comparação de encadeamentos pode produzirfalse
.Além disso, o problema de visibilidade da memória é relevante aqui, não há garantia de que as alterações
a
feitas por outro encadeamento sejam visíveis para o encadeamento atual.fonte
!=
, que envolve carregar o LHS e o RHS separadamente. Portanto, se o JLS não mencionar nada específico sobre otimizações quando o LHS e o RHS forem sintaticamente idênticos, a regra geral será aplicada, o que significa carregara
duas vezes.Se
a
potencialmente puder ser atualizado por outro thread (sem sincronização adequada!), Então Não.Isso não significa nada! O problema é que, se uma execução
a
atualizada por outro encadeamento for permitida pelo JLS, o código não será seguro para encadeamento. O fato de você não poder fazer com que a condição de corrida aconteça com um caso de teste específico em uma máquina específica e uma implementação Java específica, não impede que isso aconteça em outras circunstâncias.Sim, em teoria, sob certas circunstâncias.
Como alternativa,
a != a
poderia retornarfalse
mesmo quea
estivesse mudando simultaneamente.Em relação ao "comportamento estranho":
Esse comportamento "estranho" é consistente com o seguinte cenário de execução:
O programa é carregado e a JVM começa a interpretar os bytecodes. Como (como vimos na saída javap), o bytecode realiza duas cargas, (aparentemente) você vê os resultados da condição de corrida, ocasionalmente.
Depois de um tempo, o código é compilado pelo compilador JIT. O otimizador de JIT percebe que há duas cargas do mesmo slot de memória (
a
) próximas e otimiza o segundo. (De fato, há uma chance de otimizar completamente o teste ...)Agora a condição de corrida não se manifesta mais, porque não há mais duas cargas.
Observe que tudo isso é consistente com o que o JLS permite que uma implementação do Java faça.
@kriss comentou assim:
O Java Memory Model (especificado no JLS 17.4 ) especifica um conjunto de pré-condições sob as quais um encadeamento é garantido para ver valores de memória gravados por outro encadeamento. Se um encadeamento tentar ler uma variável escrita por outro e essas pré-condições não forem atendidas, pode haver várias execuções possíveis ... algumas das quais provavelmente incorretas (da perspectiva dos requisitos do aplicativo). Em outras palavras, o conjunto de comportamentos possíveis (ou seja, o conjunto de "execuções bem formadas") é definido, mas não podemos dizer qual desses comportamentos ocorrerá.
É permitido ao compilador combinar e reordenar cargas e salvar (e fazer outras coisas), desde que o efeito final do código seja o mesmo:
Mas se o código não sincronizar corretamente (e, portanto, os relacionamentos "acontecer antes" não restringirem suficientemente o conjunto de execuções bem formadas), o compilador poderá reordenar cargas e armazenamentos de maneira a fornecer resultados "incorretos". (Mas isso significa apenas que o programa está incorreto.)
fonte
a != a
poderia retornar verdadeiro?Provado com teste-ng:
Eu tenho 2 falhas em 10.000 invocações. Portanto, NÃO , é NÃO thread-safe
fonte
Random.nextInt()
parte é supérflua. Você poderia ter testadonew Object()
também.Não não é. Para uma comparação, a Java VM deve colocar os dois valores a serem comparados na pilha e executar a instrução compare (que depende do tipo de "a").
A Java VM pode:
false
No 1º caso, outro encadeamento pode modificar o valor de "a" entre as duas leituras.
A estratégia escolhida depende do compilador Java e do Java Runtime (especialmente o compilador JIT). Pode até mudar durante o tempo de execução do seu programa.
Se você quiser ter certeza de como a variável é acessada, faça-a
volatile
(a chamada "barreira da meia memória") ou adicione uma barreira de memória completa (synchronized
). Você também pode usar alguma API de nível superior (por exemplo,AtomicInteger
como mencionado por Juned Ahasan).Para obter detalhes sobre segurança de encadeamento, leia JSR 133 ( Java Memory Model ).
fonte
a
comovolatile
ainda implicaria duas leituras distintas, com a possibilidade de uma mudança intermediária.Tudo foi bem explicado por Stephen C. Por diversão, você pode tentar executar o mesmo código com os seguintes parâmetros da JVM:
Isso deve impedir a otimização feita pelo JIT (no servidor do hotspot 7) e você verá
true
para sempre (parei em 2.000.000, mas suponho que continue depois disso).Para obter informações, abaixo está o código JIT. Para ser sincero, não leio a montagem com fluência suficiente para saber se o teste foi realmente realizado ou de onde vêm as duas cargas. (a linha 26 é o teste
flag = a != a
e a linha 31 é a chave de fechamento dowhile(true)
).fonte
0x27dccd1
para0x27dccdf
. Ojmp
loop in é incondicional (já que o loop é infinito). As únicas outras duas instruções no loop sãoadd rbc, 0x1
- o que está aumentandocountOfIterations
(apesar do fato de o loop nunca ser encerrado para que esse valor não seja lido: talvez seja necessário caso você o invista no depurador), .. .test
instrução de aparência estranha , que na verdade existe apenas para o acesso à memória (observe que issoeax
nunca é definido no método!): é uma página especial configurada como não legível quando a JVM deseja acionar todos os threads para alcançar um ponto seguro, para que ele possa executar gc ou alguma outra operação que exija que todos os threads estejam em um estado conhecido.instance. a != instance.a
comparação do loop e a executou apenas uma vez, antes de o loop ser inserido! Ele sabe que não é necessário recarregarinstance
oua
como eles não são declarados voláteis e não há outro código que possa alterá-los no mesmo encadeamento, portanto, apenas assume que eles são os mesmos durante todo o loop, o que é permitido pela memória modelo.Não, o
a != a
thread não é seguro. Essa expressão consiste em três partes: carregara
, carregara
novamente e executar!=
. É possível que outro encadeamento obtenha o bloqueio intrínseco noa
pai e altere o valora
entre as duas operações de carregamento.Outro fator, porém, é se
a
é local. Sea
for local, nenhum outro thread deve ter acesso a ele e, portanto, deve ser seguro para threads.também deve sempre imprimir
false
.Declarar
a
comovolatile
não resolveria o problema para ifa
isstatic
ou instance. O problema não é que os segmentos tenham valores diferentes dea
, mas que um segmento seja carregadoa
duas vezes com valores diferentes. Na verdade, pode tornar o caso menos seguro para threads. Sea
não estivervolatile
,a
pode ser armazenado em cache e uma alteração em outro thread não afetará o valor armazenado em cache.fonte
synchronized
está errado: para garantir a impressão desse códigofalse
, todos os métodos definidos tambéma
deveriam sersynchronized
.a
pai enquanto o método estiver em execução, necessário para definir o valora
.Em relação ao comportamento estranho:
Como a variável
a
não está marcada comovolatile
, em algum momento, seu valora
pode ser armazenado em cache pelo encadeamento. Ambosa
sa != a
são então a versão em cache e, portanto, sempre a mesma (o significadoflag
é agora semprefalse
).fonte
Mesmo a leitura simples não é atômica. Se
a
estálong
e não está marcado comovolatile
nas JVMs de 32 bits,long b = a
não é seguro para threads.fonte