O segmento de verificação! = É seguro?

140

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 falsee, em seguida, a execução do programa por um longo período de tempo não gera uma única truesaí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
Narendra Pathai
fonte
2
O que você quer dizer com "thread-safe" neste contexto? Você está perguntando se é garantido que sempre retorne falso?
JB Nizet
@JBNizet yes. Era nisso que eu estava pensando.
Narendra Pathai
5
Nem sempre retorna false em um contexto de thread único. Poderia ser um NaN ..
Harold
4
Explicação provável: o código foi compilado just-in-time e o código compilado carrega a variável apenas uma vez. Isso é esperado.
Marko Topolnik
3
Imprimir resultados individuais é uma maneira ruim de testar as corridas. A impressão (formatação e gravação dos resultados) é relativamente cara em comparação com o seu teste (e algumas vezes o programa acaba bloqueando a gravação quando a largura de banda da conexão ao terminal ou o próprio terminal é lenta). Além disso, IO muitas vezes contém mutexes de seu próprio que irá permutar a ordem de execução dos seus tópicos (observe suas linhas individuais de 1234:truenã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).
Ben Jackson

Respostas:

124

Na ausência de sincronização, este código

Object a;

public boolean test() {
    return a != a;
}

pode produzir true. Este é o bytecode paratest()

    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    ALOAD 0
    GETFIELD test/Test1.a : Ljava/lang/Object;
    IF_ACMPEQ L1
...

como podemos ver, ele carrega o campo apara os vars locais duas vezes, é uma operação não atômica, se tiver asido alterada no meio por outra comparação de encadeamentos pode produzir false.

Além disso, o problema de visibilidade da memória é relevante aqui, não há garantia de que as alterações afeitas por outro encadeamento sejam visíveis para o encadeamento atual.

Evgeniy Dorofeev
fonte
22
Embora seja uma evidência forte, o bytecode não é realmente uma prova. Ele deve estar em algum lugar nas JLS, bem ...
Marko Topolnik
10
@ Marko Eu concordo com o seu pensamento, mas não necessariamente com a sua conclusão. Para mim, o bytecode acima é a maneira óbvia / canônica de implementar !=, 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 carregar aduas vezes.
Andrzej Doyle
20
Na verdade, assumindo que o bytecode gerado esteja em conformidade com o JLS, é uma prova!
proskor
6
@Adrian: Primeiramente: mesmo que essa suposição seja inválida, a existência de um único compilador em que possa ser avaliado como "verdadeiro" é suficiente para demonstrar que às vezes é possível avaliar como "verdadeiro" (mesmo que a especificação o proibisse - o que não). Segundo: o Java é bem especificado, e a maioria dos compiladores está em conformidade com ele. Faz sentido usá-los como referência a esse respeito. Terceiro: você usa o termo "JRE", mas não acho que isso signifique o que você pensa que significa. . .
Ruakh 27/08
2
@AlexanderTorstling - "Não tenho certeza se isso é suficiente para impedir uma otimização de leitura única." Não é suficiente. De fato, na ausência de sincronização (e os relacionamentos extras "acontecem antes" que impõem), a otimização é válida,
Stephen C -
47

O cheque é a != aseguro?

Se apotencialmente puder ser atualizado por outro thread (sem sincronização adequada!), Então Não.

Tentei programar isso e usar vários threads, mas não falhei. Acho que não consegui simular a corrida na minha máquina.

Isso não significa nada! O problema é que, se uma execução aatualizada 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.

Isso significa que a! = A poderia retornar true.

Sim, em teoria, sob certas circunstâncias.

Como alternativa, a != apoderia retornar falsemesmo que aestivesse mudando simultaneamente.


Em relação ao "comportamento estranho":

Quando meu programa inicia entre algumas iterações, obtenho o valor do sinalizador de saída, o que significa que a verificação de reference! = 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 tempo não gera uma única saída verdadeira.

Esse comportamento "estranho" é consistente com o seguinte cenário de execução:

  1. 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.

  2. 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 ...)

  3. 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:

Parece que isso pode ser o que os programadores de C ou C ++ chamam de "comportamento indefinido" (dependente da implementação). Parece que pode haver alguns UB em java em casos de canto como este.

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:

  • quando executado por um único encadeamento, e
  • quando executado por diferentes threads que são sincronizados corretamente (conforme o modelo de memória).

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.)

Stephen C
fonte
Isso significa que a != apoderia retornar verdadeiro?
proskor
Eu quis dizer que talvez na minha máquina eu não pudesse simular que o código acima não é seguro para threads. Então, talvez haja um raciocínio teórico por trás disso.
Narendra Pathai 27/08/13
@NarendraPathai - Não há razão teórica para você não demonstrar. Existe possivelmente uma razão prática ... ou talvez você simplesmente não tenha tido sorte.
Stephen C
Por favor, verifique minha resposta atualizada com o programa que estou usando. Às vezes, a verificação retorna verdadeira, mas parece haver um comportamento estranho na saída.
Narendra Pathai
1
@NarendraPathai - Veja minha explicação.
Stephen C
27

Provado com teste-ng:

public class MyTest {

  private static Integer count=1;

  @Test(threadPoolSize = 1000, invocationCount=10000)
  public void test(){
    count = new Integer(new Random().nextInt());
    Assert.assertFalse(count != count);
  }

}

Eu tenho 2 falhas em 10.000 invocações. Portanto, NÃO , é NÃO thread-safe

Arnaud Denoyelle
fonte
6
Você nem está procurando igualdade ... a Random.nextInt()parte é supérflua. Você poderia ter testado new Object()também.
Marko Topolnik
@MarkoTopolnik Verifique minha resposta atualizada com o programa que estou usando. Às vezes, a verificação retorna verdadeira, mas parece haver um comportamento estranho na saída.
Narendra Pathai
1
Uma observação: objetos aleatórios geralmente devem ser reutilizados, não criados toda vez que você precisa de um novo int.
Simon Forsberg
15

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:

  1. Leia "a" duas vezes, coloque cada um na pilha e compare os resultados
  2. Leia "a" apenas uma vez, coloque-o na pilha, duplique-o (instrução "dup") e execute a comparação
  3. Elimine a expressão completamente e substitua-a por 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, AtomicIntegercomo mencionado por Juned Ahasan).

Para obter detalhes sobre segurança de encadeamento, leia JSR 133 ( Java Memory Model ).

stefan.schwetschke
fonte
Declarar acomo volatileainda implicaria duas leituras distintas, com a possibilidade de uma mudança intermediária.
Holger
6

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:

-XX:InlineSmallCode=0

Isso deve impedir a otimização feita pelo JIT (no servidor do hotspot 7) e você verá truepara 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 != ae a linha 31 é a chave de fechamento do while(true)).

  # {method} 'run' '()V' in 'javaapplication27/TestThreadSafety$1'
  0x00000000027dcc80: int3   
  0x00000000027dcc81: data32 data32 nop WORD PTR [rax+rax*1+0x0]
  0x00000000027dcc8c: data32 data32 xchg ax,ax
  0x00000000027dcc90: mov    DWORD PTR [rsp-0x6000],eax
  0x00000000027dcc97: push   rbp
  0x00000000027dcc98: sub    rsp,0x40
  0x00000000027dcc9c: mov    rbx,QWORD PTR [rdx+0x8]
  0x00000000027dcca0: mov    rbp,QWORD PTR [rdx+0x18]
  0x00000000027dcca4: mov    rcx,rdx
  0x00000000027dcca7: movabs r10,0x6e1a7680
  0x00000000027dccb1: call   r10
  0x00000000027dccb4: test   rbp,rbp
  0x00000000027dccb7: je     0x00000000027dccdd
  0x00000000027dccb9: mov    r10d,DWORD PTR [rbp+0x8]
  0x00000000027dccbd: cmp    r10d,0xefc158f4    ;   {oop('javaapplication27/TestThreadSafety$1')}
  0x00000000027dccc4: jne    0x00000000027dccf1
  0x00000000027dccc6: test   rbp,rbp
  0x00000000027dccc9: je     0x00000000027dcce1
  0x00000000027dcccb: cmp    r12d,DWORD PTR [rbp+0xc]
  0x00000000027dcccf: je     0x00000000027dcce1  ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd1: add    rbx,0x1            ; OopMap{rbp=Oop off=85}
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
  0x00000000027dccd5: test   DWORD PTR [rip+0xfffffffffdb53325],eax        # 0x0000000000330000
                                                ;*goto
                                                ; - javaapplication27.TestThreadSafety$1::run@62 (line 31)
                                                ;   {poll}
  0x00000000027dccdb: jmp    0x00000000027dccd1
  0x00000000027dccdd: xor    ebp,ebp
  0x00000000027dccdf: jmp    0x00000000027dccc6
  0x00000000027dcce1: mov    edx,0xffffff86
  0x00000000027dcce6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dcceb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=112}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dccf0: int3   
  0x00000000027dccf1: mov    edx,0xffffffad
  0x00000000027dccf6: mov    QWORD PTR [rsp+0x20],rbx
  0x00000000027dccfb: call   0x00000000027a90a0  ; OopMap{rbp=Oop off=128}
                                                ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
                                                ;   {runtime_call}
  0x00000000027dcd00: int3                      ;*aload_0
                                                ; - javaapplication27.TestThreadSafety$1::run@2 (line 26)
  0x00000000027dcd01: int3   
assylias
fonte
1
Este é um bom exemplo do tipo de código que a JVM realmente produzirá quando você tiver um loop infinito e tudo pode ser mais ou menos içado. O "loop" real aqui estão as três instruções de 0x27dccd1para 0x27dccdf. O jmploop in é incondicional (já que o loop é infinito). As únicas outras duas instruções no loop são add rbc, 0x1- o que está aumentando countOfIterations(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), .. .
BeeOnRope
... e a testinstrução de aparência estranha , que na verdade existe apenas para o acesso à memória (observe que isso eaxnunca é 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.
BeeOnRope
Mais exatamente, a JVM içou completamente a instance. a != instance.acomparação do loop e a executou apenas uma vez, antes de o loop ser inserido! Ele sabe que não é necessário recarregar instanceou acomo 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.
BeeOnRope 15/05
5

Não, o a != athread não é seguro. Essa expressão consiste em três partes: carregar a, carregar anovamente e executar !=. É possível que outro encadeamento obtenha o bloqueio intrínseco no apai e altere o valor aentre as duas operações de carregamento.

Outro fator, porém, é se aé local. Se afor local, nenhum outro thread deve ter acesso a ele e, portanto, deve ser seguro para threads.

void method () {
    int a = 0;
    System.out.println(a != a);
}

também deve sempre imprimir false.

Declarar acomo volatilenão resolveria o problema para if ais staticou instance. O problema não é que os segmentos tenham valores diferentes de a, mas que um segmento seja carregado aduas vezes com valores diferentes. Na verdade, pode tornar o caso menos seguro para threads. Se anão estiver volatile, apode ser armazenado em cache e uma alteração em outro thread não afetará o valor armazenado em cache.

DoubleMx2
fonte
Seu exemplo com synchronizedestá errado: para garantir a impressão desse código false, todos os métodos definidos também a deveriam ser synchronized.
Ruakh 27/08
Por quê então? Se o método estiver sincronizado, como qualquer outro encadeamento obterá o bloqueio intrínseco no apai enquanto o método estiver em execução, necessário para definir o valor a.
DoubleMx2 27/08
1
Suas instalações estão erradas. Você pode definir o campo de um objeto sem adquirir seu bloqueio intrínseco. Java não requer um encadeamento para adquirir o bloqueio intrínseco de um objeto antes de definir seus campos.
Ruakh 27/08
3

Em relação ao comportamento estranho:

Como a variável anão está marcada como volatile, em algum momento, seu valor apode ser armazenado em cache pelo encadeamento. Ambos as a != asão então a versão em cache e, portanto, sempre a mesma (o significado flagé agora sempre false).

Walter Laan
fonte
0

Mesmo a leitura simples não é atômica. Se aestá longe não está marcado como volatilenas JVMs de 32 bits, long b = anão é seguro para threads.

ZhekaKozlov
fonte
volátil e atomicidade não têm relação. mesmo se eu marcar um volátil será não-atômica
Narendra Pathai
A atribuição de um campo longo volátil é sempre atômica. As outras operações como ++ não são.
ZhekaKozlov