Porque é i++
não atômico em Java?
Para me aprofundar um pouco mais em Java, tentei contar quantas vezes o loop em threads é executado.
Então eu usei um
private static int total = 0;
na classe principal.
Eu tenho dois tópicos.
- Tópico 1: Impressões
System.out.println("Hello from Thread 1!");
- Tópico 2: Impressões
System.out.println("Hello from Thread 2!");
E eu conto as linhas impressas pela linha 1 e linha 2. Mas as linhas da linha 1 + linhas da linha 2 não correspondem ao número total de linhas impressas.
Aqui está o meu código:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Test {
private static int total = 0;
private static int countT1 = 0;
private static int countT2 = 0;
private boolean run = true;
public Test() {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
newCachedThreadPool.execute(t1);
newCachedThreadPool.execute(t2);
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
run = false;
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Logger.getLogger(Test.class.getName()).log(Level.SEVERE, null, ex);
}
System.out.println((countT1 + countT2 + " == " + total));
}
private Runnable t1 = new Runnable() {
@Override
public void run() {
while (run) {
total++;
countT1++;
System.out.println("Hello #" + countT1 + " from Thread 2! Total hello: " + total);
}
}
};
private Runnable t2 = new Runnable() {
@Override
public void run() {
while (run) {
total++;
countT2++;
System.out.println("Hello #" + countT2 + " from Thread 2! Total hello: " + total);
}
}
};
public static void main(String[] args) {
new Test();
}
}
java
multithreading
concurrency
Andie2302
fonte
fonte
AtomicInteger
?iinc
operação para incrementar inteiros, mas isso só funciona para variáveis locais, onde a simultaneidade não é uma preocupação. Para campos, o compilador gera comandos de leitura-modificação-gravação separadamente.iinc
instrução para os campos, ter uma única instrução não garante a atomicidade, por exemplo, o acesso nãovolatile
long
e aodouble
campo não é garantido como atômico, independentemente do fato de ser executado por uma única instrução bytecode.Respostas:
i++
provavelmente não é atômico em Java porque a atomicidade é um requisito especial que não está presente na maioria dos usos dei++
. Esse requisito tem uma sobrecarga significativa: há um grande custo em tornar uma operação de incremento atômica; envolve a sincronização nos níveis de software e hardware que não precisam estar presentes em um incremento comum.Você poderia apresentar o argumento que
i++
deveria ter sido projetado e documentado como especificamente realizando um incremento atômico, de forma que um incremento não atômico seja executado usandoi = i + 1
. No entanto, isso quebraria a "compatibilidade cultural" entre Java e C e C ++. Da mesma forma, retiraria uma notação conveniente que os programadores familiarizados com as linguagens do tipo C tomam como certa, dando a ela um significado especial que se aplica apenas em circunstâncias limitadas.Código básico C ou C ++ like
for (i = 0; i < LIMIT; i++)
seria traduzido para Java comofor (i = 0; i < LIMIT; i = i + 1)
; porque seria inapropriado usar o atômicoi++
. O que é pior, os programadores vindos de C ou outras linguagens semelhantes a C para Java usariam dei++
qualquer maneira, resultando no uso desnecessário de instruções atômicas.Mesmo no nível do conjunto de instruções da máquina, uma operação do tipo incremento geralmente não é atômica por motivos de desempenho. No x86, uma instrução especial "prefixo de bloqueio" deve ser usada para tornar a
inc
instrução atômica: pelas mesmas razões acima. E seinc
fossem sempre atômicos, nunca seria usado quando um inc não atômico é necessário; programadores e compiladores gerariam código que carrega, adiciona 1 e armazena, porque seria muito mais rápido.Em algumas arquiteturas de conjunto de instruções, não há atômico
inc
ou talvez nenhuminc
; para fazer um atômico inc no MIPS, você precisa escrever um loop de software que usa oll
andsc
: load-linked e store-condicional. Load-linked lê a palavra, e store-condicional armazena o novo valor se a palavra não mudou, ou então ele falha (o que é detectado e causa uma nova tentativa).fonte
i = i + 1
seria uma tradução para++i
, nãoi++
volatile
campos. Portanto, a menos que você trate cada campo como implicitamentevolatile
, uma vez que um thread tenha usado o++
operador nele, essa garantia de atomicidade não resolveria os problemas de atualização simultânea. Então, por que potencialmente desperdiçar desempenho por algo se isso não resolve o problema.++
? ;)i++
envolve duas operações:i
i
Quando dois threads executam
i++
na mesma variável ao mesmo tempo, eles podem obter o mesmo valor atual dei
e, em seguida, incrementar e configurá-lo parai+1
, então você obterá uma única incrementação em vez de duas.Exemplo:
fonte
i++
fosse atômico, não seria um comportamento bem definido / seguro para thread.)O importante é o JLS (Java Language Specification), e não como várias implementações do JVM podem ou não ter implementado um determinado recurso da linguagem. O JLS define o operador ++ postfix na cláusula 15.14.2 que diz ia "o valor 1 é adicionado ao valor da variável e a soma é armazenada de volta na variável". Em nenhum lugar ele menciona ou sugere multithreading ou atomicidade. Para estes o JLS fornece voláteis e sincronizados . Além disso, existe o pacote java.util.concurrent.atomic (consulte http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/atomic/package-summary.html )
fonte
Vamos quebrar a operação de incremento em várias instruções:
Tópico 1 e 2:
Se não houver sincronização, digamos que o Thread um leu o valor 3 e o incrementou para 4, mas não o escreveu de volta. Nesse ponto, ocorre a troca de contexto. O thread dois lê o valor 3, o incrementa e a troca de contexto acontece. Embora ambos os threads tenham incrementado o valor total, ainda será uma condição de 4 corridas.
fonte
i++
é uma declaração que envolve simplesmente 3 operações:Essas três operações não devem ser executadas em uma única etapa ou em outras palavras
i++
não é um composto operação . Como resultado, todo tipo de coisa pode dar errado quando mais de um encadeamento está envolvido em uma operação única, mas não composta.Considere o seguinte cenário:
Tempo 1 :
Tempo 2 :
Tempo 3 :
E aí está. Uma condição de corrida.
É por isso que
i++
não é atômico. Se fosse, nada disso teria acontecido e cadafetch-update-store
um aconteceria atomicamente. Isso é exatamente o queAtomicInteger
para serve e, no seu caso, provavelmente se encaixaria perfeitamente.PS
Um excelente livro cobrindo todas essas questões e mais algumas é: Java Concurrency in Practice
fonte
i++
não é atômico.Na JVM, um incremento envolve uma leitura e uma gravação, portanto, não é atômico.
fonte
Se a operação
i++
fosse atômica, você não teria a chance de ler o valor dela. Isso é exatamente o que você deseja fazer usandoi++
(em vez de usar++i
).Por exemplo, observe o seguinte código:
Neste caso, esperamos que a saída seja:
0
(porque postamos incremento, por exemplo, primeiro lemos e depois atualizamos)Esta é uma das razões pelas quais a operação não pode ser atômica, porque você precisa ler o valor (e fazer algo com ele) e então atualizar o valor.
A outra razão importante é que fazer algo atomicamente geralmente leva mais tempo por causa do bloqueio. Seria idiota se todas as operações em primitivas demorassem um pouco mais nos raros casos em que as pessoas desejam realizar operações atômicas. É por isso que eles adicionaram
AtomicInteger
e outras aulas Atómica à linguagem.fonte
i++
para ser expandidai.getAndIncrement()
. Essa expansão não é nova. Por exemplo, lambdas em C ++ são expandidos para definições de classes anônimas em C ++.i++
pode-se criar trivialmente um atômico++i
ou vice-versa. Um é equivalente ao outro mais um.Existem duas etapas:
então não é uma operação atômica. Quando thread1 executa i ++ e thread2 executa i ++, o valor final de i pode ser i + 1.
fonte
Simultaneidade (a
Thread
classe e tal) é um recurso adicionado na v1.0 do Java .i++
foi adicionado na versão beta antes disso e, como tal, ainda é mais do que provável em sua implementação original (mais ou menos).Cabe ao programador sincronizar as variáveis. Confira o tutorial da Oracle sobre isso .
Edit: Para esclarecer, i ++ é um procedimento bem definido que antecede o Java e, como tal, os designers de Java decidiram manter a funcionalidade original desse procedimento.
O operador ++ foi definido em B (1969), que antecede java e threading por apenas um pouco.
fonte
i++
não ser atômico é uma decisão de design, não um descuido em um sistema em crescimento.