Por que i ++ não é atômico?

97

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();
    }
}
Andie2302
fonte
14
Por que você não tenta AtomicInteger?
Braj
3
a JVM tem uma iincoperaçã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.
Silly Freak
14
Por que você esperaria que fosse atômico?
Hot Licks de
2
@Silly Freak: mesmo que houvesse uma iincinstrução para os campos, ter uma única instrução não garante a atomicidade, por exemplo, o acesso não volatile longe ao doublecampo não é garantido como atômico, independentemente do fato de ser executado por uma única instrução bytecode.
Holger

Respostas:

125

i++ provavelmente não é atômico em Java porque a atomicidade é um requisito especial que não está presente na maioria dos usos de i++ . 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 usando i = 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 como for (i = 0; i < LIMIT; i = i + 1); porque seria inapropriado usar o atômico i++. O que é pior, os programadores vindos de C ou outras linguagens semelhantes a C para Java usariam de i++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 incinstruçã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 incou talvez nenhum inc; para fazer um atômico inc no MIPS, você precisa escrever um loop de software que usa o lland sc: 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).

Kaz
fonte
2
como java não tem ponteiros, incrementar variáveis ​​locais é inerentemente thread save, então com loops o problema não seria tão ruim. seu ponto sobre menos surpresa permanece, é claro. também, como está, i = i + 1seria uma tradução para ++i, nãoi++
Silly Freak
22
A primeira palavra da pergunta é "por quê". A partir de agora, esta é a única resposta para abordar a questão do "por quê". As outras respostas apenas reafirmam a questão. Então, +1.
Dawood ibn Kareem
3
Pode ser interessante notar que uma garantia de atomicidade não resolveria o problema de visibilidade para atualizações de não volatilecampos. Portanto, a menos que você trate cada campo como implicitamente volatile, 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.
Holger
1
@DavidWallace não quer dizer ++? ;)
Dan Hlavenka de
36

i++ envolve duas operações:

  1. leia o valor atual de i
  2. incrementar o valor e atribuí-lo a i

Quando dois threads executam i++na mesma variável ao mesmo tempo, eles podem obter o mesmo valor atual de ie, em seguida, incrementar e configurá-lo para i+1, então você obterá uma única incrementação em vez de duas.

Exemplo:

int i = 5;
Thread 1 : i++;
           // reads value 5
Thread 2 : i++;
           // reads value 5
Thread 1 : // increments i to 6
Thread 2 : // increments i to 6
           // i == 6 instead of 7
Eran
fonte
(Mesmo se i++ fosse atômico, não seria um comportamento bem definido / seguro para thread.)
user2864740
15
+1, mas "1. A, 2. B e C" soa como três operações, não duas. :)
yshavit
3
Observe que mesmo se a operação fosse implementada com uma única instrução de máquina que incrementasse um local de armazenamento no local, não há garantia de que seria thread-safe. A máquina ainda precisa buscar o valor, incrementá-lo e armazená-lo de volta, além disso, pode haver várias cópias de cache desse local de armazenamento.
Hot Licks de
3
@Aquarelle - Se dois processadores executam a mesma operação no mesmo local de armazenamento simultaneamente, e não há transmissão de "reserva" no local, é quase certo que eles irão interferir e produzir resultados falsos. Sim, é possível que esta operação seja "segura", mas exige um esforço especial, mesmo ao nível do hardware.
Hot Licks
6
Mas acho que a questão era "Por que" e não "O que acontece".
Sebastian Mach
11

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 )

Jonathan Rosenne
fonte
5

Por que i ++ não é atômico em Java?

Vamos quebrar a operação de incremento em várias instruções:

Tópico 1 e 2:

  1. Busca o valor total da memória
  2. Adicione 1 ao valor
  3. Escreva de volta para a memória

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.

Aniket Thakur
fonte
2
Não entendo como isso deve ser uma resposta à pergunta. Uma linguagem pode definir qualquer característica como atômica, sejam incrementos ou unicórnios. Você acabou de exemplificar uma consequência de não ser atômico.
Sebastian Mach
Sim, uma linguagem pode definir qualquer recurso como atômico, mas no que diz respeito a java é considerado operador de incremento (que é a questão postada pelo OP) não é atômico e minha resposta indica as razões.
Aniket Thakur
1
(desculpe pelo meu tom áspero no primeiro comentário) Mas então, o motivo parece ser "porque se fosse atômico, então não haveria condições de corrida". Ou seja, parece que uma condição de corrida é desejável.
Sebastian Mach
@phresnel o overhead introduzido para manter um incremento atômico é enorme e raramente desejado, mantendo a operação barata e como resultado não atômico é desejável na maioria das vezes.
Josefx
4
@josefx: Observe que não estou questionando os fatos, mas o raciocínio desta resposta. Basicamente, ele diz "i ++ não é atômico em Java por causa das condições de corrida que tem" , o que é como dizer "um carro não tem airbag por causa dos acidentes que podem acontecer" ou "você não ganha uma faca com seu pedido de currywurst porque o pode ser necessário cortar a salsicha " . Portanto, não acho que seja uma resposta. A pergunta não era "O que o i ++ faz?" ou "Qual é a consequência de i ++ não ser sincronizado?" .
Sebastian Mach
5

i++ é uma declaração que envolve simplesmente 3 operações:

  1. Leia o valor atual
  2. Escreva um novo valor
  3. Armazenar novo valor

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 :

Thread A fetches i
Thread B fetches i

Tempo 2 :

Thread A overwrites i with a new value say -foo-
Thread B overwrites i with a new value say -bar-
Thread B stores -bar- in i

// At this time thread B seems to be more 'active'. Not only does it overwrite 
// its local copy of i but also makes it in time to store -bar- back to 
// 'main' memory (i)

Tempo 3 :

Thread A attempts to store -foo- in memory effectively overwriting the -bar- 
value (in i) which was just stored by thread B in Time 2.

Thread B has nothing to do here. Its work was done by Time 2. However it was 
all for nothing as -bar- was eventually overwritten by another thread.

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

kstratis
fonte
1
Hmm. Uma linguagem pode definir qualquer característica como atômica, sejam incrementos ou unicórnios. Você acabou de exemplificar uma consequência de não ser atômico.
Sebastian Mach
@phresnel Exatamente. Mas também aponto que não é uma única operação que, por extensão, implica que o custo computacional para transformar várias dessas operações em atômicas é muito mais caro, o que por sua vez - parcialmente - justifica por que i++não é atômico.
kstratis
1
Embora eu entenda seu ponto, sua resposta é um pouco confusa para o aprendizado. Eu vejo um exemplo e uma conclusão que diz "por causa da situação no exemplo"; imho, este é um raciocínio incompleto :(
Sebastian Mach
1
@phresnel Talvez não seja a resposta mais pedagógica, mas é o melhor que posso oferecer atualmente. Espero que ajude as pessoas e não as confunda. Obrigado pela crítica no entanto. Vou tentar ser mais preciso em minhas próximas postagens.
kstratis
2

Na JVM, um incremento envolve uma leitura e uma gravação, portanto, não é atômico.

celeritas
fonte
2

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 usando i++(em vez de usar ++i).

Por exemplo, observe o seguinte código:

public static void main(final String[] args) {
    int i = 0;
    System.out.println(i++);
}

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 AtomicIntegere outras aulas Atómica à linguagem.

Roy van Rijn
fonte
2
Isso é enganoso. Você tem que separar a execução e obter o resultado, caso contrário, você não poderia obter valores de nenhuma operação atômica.
Sebastian Mach
Não, não é, é por isso que o AtomicInteger do Java tem get (), getAndIncrement (), getAndDecrement (), incrementAndGet (), decrementAndGet () etc.
Roy van Rijn
1
E a linguagem Java poderia ser definida i++para ser expandida i.getAndIncrement(). Essa expansão não é nova. Por exemplo, lambdas em C ++ são expandidos para definições de classes anônimas em C ++.
Sebastian Mach
Dado um atômico, i++pode-se criar trivialmente um atômico ++iou vice-versa. Um é equivalente ao outro mais um.
David Schwartz
2

Existem duas etapas:

  1. buscar eu da memória
  2. defina i + 1 para i

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.

Yanghaogn
fonte
-1

Simultaneidade (a Threadclasse 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.

O morcego
fonte
-1 "Tópico de classe pública ... Desde: JDK1.0" Fonte: docs.oracle.com/javase/7/docs/api/index.html?java/lang/…
Silly Freak
A versão não importa tanto quanto o fato de que ainda foi implementada antes da classe Thread e não foi alterada por causa disso, mas eu editei minha resposta para agradá-lo.
TheBat
5
O que importa é que sua afirmação "ainda foi implementado antes da classe Thread" não é apoiada por fontes. i++não ser atômico é uma decisão de design, não um descuido em um sistema em crescimento.
Silly Freak
Lol isso é fofo. i ++ foi definido bem antes de Threads, simplesmente porque havia linguagens que existiam antes do Java. Os criadores do Java usaram essas outras linguagens como base, em vez de redefinir um procedimento bem aceito. Onde eu já disse que foi um descuido?
TheBat
@SillyFreak Aqui estão algumas fontes que mostram a idade de ++: en.wikipedia.org/wiki/Increment_and_decrement_operators en.wikipedia.org/wiki/B_(programming_language)
TheBat