Usos práticos para AtomicInteger

229

Eu meio que entendo que AtomicInteger e outras variáveis ​​atômicas permitem acessos simultâneos. Em que casos essa classe é normalmente usada?

James P.
fonte

Respostas:

190

Existem dois usos principais de AtomicInteger:

  • Como um contador atômico ( incrementAndGet(), etc) que pode ser usado por muitos threads simultaneamente

  • Como uma primitiva que suporta instruções de comparação e troca ( compareAndSet()) para implementar algoritmos sem bloqueio.

    Aqui está um exemplo de gerador de números aleatórios sem bloqueio da Java Concurrency Na Prática de Brian Göetz :

    public class AtomicPseudoRandom extends PseudoRandom {
        private AtomicInteger seed;
        AtomicPseudoRandom(int seed) {
            this.seed = new AtomicInteger(seed);
        }
    
        public int nextInt(int n) {
            while (true) {
                int s = seed.get();
                int nextSeed = calculateNext(s);
                if (seed.compareAndSet(s, nextSeed)) {
                    int remainder = s % n;
                    return remainder > 0 ? remainder : remainder + n;
                }
            }
        }
        ...
    }

    Como você pode ver, basicamente funciona quase da mesma maneira que incrementAndGet(), mas executa cálculos arbitrários ( calculateNext()) em vez de incrementar (e processa o resultado antes do retorno).

axtavt
fonte
1
Eu acho que entendo o primeiro uso. Isso é para garantir que o contador tenha sido incrementado antes que um atributo seja acessado novamente. Corrigir? Você poderia dar um pequeno exemplo para o segundo uso?
James P.
8
Seu entendimento do primeiro uso é meio verdadeiro - ele simplesmente garante que, se outro thread modificar o contador entre as operações reade write that value + 1, isso será detectado em vez de substituir a atualização antiga (evitando o problema de "atualização perdida"). Este é realmente um caso especial de compareAndSet- se o valor antigo era 2, a classe realmente chama compareAndSet(2, 3)-, se outro encadeamento modificou o valor nesse meio tempo, o método de incremento é reiniciado efetivamente desde o início.
Andrzej Doyle
3
"restante> 0? restante: restante + n;" nessa expressão, há uma razão para adicionar o restante a n quando é 0?
Sandeepkunkunuru
100

O exemplo mais simples e absoluto que consigo pensar é tornar o incremento uma operação atômica.

Com entradas padrão:

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; // Not atomic, multiple threads could get the same result
}

Com AtomicInteger:

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

A última é uma maneira muito simples de executar efeitos simples de mutações (especialmente contagem ou indexação exclusiva), sem ter que recorrer à sincronização de todo acesso.

Uma lógica mais complexa, livre de sincronização, pode ser empregada usando compareAndSet()como um tipo de bloqueio otimista - obtenha o valor atual, calcule o resultado com base nisso, defina esse resultado se o valor ainda for a entrada usada para o cálculo, mas comece novamente - mas o exemplos de contagem são muito úteis, e eu usarei frequentemente AtomicIntegerspara contar e geradores exclusivos para toda a VM, se houver alguma sugestão de vários encadeamentos envolvidos, porque são tão fáceis de trabalhar, que eu quase consideraria otimização prematura usar ints.

Embora você quase sempre possa obter as mesmas garantias de sincronização intse synchronizeddeclarações apropriadas , a vantagem AtomicIntegeré que a segurança do encadeamento é incorporada ao próprio objeto real, em vez de você precisar se preocupar com as possíveis intercalações e monitores mantidos, de todos os métodos isso acontece para acessar o intvalor. É muito mais difícil violar acidentalmente a segurança da thread ao ligar do getAndIncrement()que ao retornar i++e lembrar (ou não) de adquirir o conjunto correto de monitores com antecedência.

Andrzej Doyle
fonte
2
Obrigado por esta explicação clara. Quais seriam as vantagens de usar um AtomicInteger sobre uma classe em que todos os métodos são sincronizados? Este último seria considerado "mais pesado"?
James P.
3
Na minha perspectiva, é principalmente o encapsulamento que você obtém com o AtomicIntegers - a sincronização ocorre exatamente do que você precisa e você obtém métodos descritivos que estão na API pública para explicar qual é o resultado pretendido. (Além disso, até certo ponto, você tem razão, muitas vezes acabamos simplesmente sincronizando todos os métodos em uma classe que provavelmente é de granulação grossa, embora o HotSpot execute otimizações de bloqueio e as regras contra a otimização prematura, considero a legibilidade um maior benefício que o desempenho.)
Andrzej Doyle
Esta é uma explicação muito clara e precisa, obrigado !!
Akash5288
Finalmente, uma explicação que esclareceu adequadamente para mim.
Benny Bottema
58

Se você observar os métodos do AtomicInteger, notará que eles tendem a corresponder a operações comuns em ints. Por exemplo:

static AtomicInteger i;

// Later, in a thread
int current = i.incrementAndGet();

é a versão segura para threads:

static int i;

// Later, in a thread
int current = ++i;

Os métodos mapeiam assim:
++iis i.incrementAndGet()
i++is i.getAndIncrement()
--iis i.decrementAndGet()
i--is i.getAndDecrement()
i = xis i.set(x)
x = iisx = i.get()

Existem outros métodos de conveniência, como compareAndSetouaddAndGet

Powerlord
fonte
37

O principal uso AtomicIntegeré quando você está em um contexto multithread e precisa executar operações seguras de encadeamento em um número inteiro sem usar synchronized. A atribuição e recuperação no tipo primitivo intjá são atômicas, mas AtomicIntegervêm com muitas operações que não são atômicas int.

Os mais simples são os getAndXXXou xXXAndGet. Por exemplo, getAndIncrement()é um equivalente atômico ao i++qual não é atômico, porque na verdade é um atalho para três operações: recuperação, adição e atribuição. compareAndSeté muito útil para implementar semáforos, fechaduras, travas etc.

Usar o AtomicIntegeré mais rápido e legível do que executar o mesmo usando a sincronização.

Um teste simples:

public synchronized int incrementNotAtomic() {
    return notAtomic++;
}

public void performTestNotAtomic() {
    final long start = System.currentTimeMillis();
    for (int i = 0 ; i < NUM ; i++) {
        incrementNotAtomic();
    }
    System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}

public void performTestAtomic() {
    final long start = System.currentTimeMillis();
    for (int i = 0 ; i < NUM ; i++) {
        atomic.getAndIncrement();
    }
    System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}

No meu PC com Java 1.6, o teste atômico é executado em 3 segundos, enquanto o sincronizado é executado em cerca de 5,5 segundos. O problema aqui é que a operação para sincronizar ( notAtomic++) é realmente curta. Portanto, o custo da sincronização é realmente importante em comparação com a operação.

Além da atomicidade, o AtomicInteger pode ser usado como uma versão mutável, Integerpor exemplo, em Maps como valores.

gabuzo
fonte
1
Acho que não gostaria de usar AtomicIntegercomo chave de mapa, porque usa a equals()implementação padrão , que quase certamente não é o que você esperaria que a semântica fosse se fosse usada em um mapa.
Andrzej Doyle
1
@ Andrzej com certeza, não como chave, que deve ser imutável, mas um valor.
gabuzo 27/01
@gabuzo Alguma idéia de por que o número inteiro atômico tem um desempenho bem mais sincronizado?
Supun Wijerathne
Agora, os testes são bastante antigos (mais de 6 anos). Pode ser interessante testar novamente com um JRE recente. Não fui aprofundado o suficiente no AtomicInteger para responder, mas como essa é uma tarefa muito específica, ela usará técnicas de sincronização que só funcionam neste caso específico. Também importa que o teste é monothreaded e fazer um teste semelhante em um ambiente carregado heaviliy pode não dar uma vitória tão clara para AtomicInteger
gabuzo
Acredito seus 3 ms e 5,5 ms
Sathesh
17

Por exemplo, eu tenho uma biblioteca que gera instâncias de alguma classe. Cada uma dessas instâncias deve ter um ID inteiro exclusivo, pois essas instâncias representam comandos sendo enviados para um servidor e cada comando deve ter um ID exclusivo. Como vários threads têm permissão para enviar comandos simultaneamente, eu uso um AtomicInteger para gerar esses IDs. Uma abordagem alternativa seria usar algum tipo de bloqueio e um número inteiro regular, mas isso é mais lento e menos elegante.

Sergei Tachenov
fonte
Obrigado por compartilhar este exemplo prático. Isso soa como algo que eu deveria usar como eu preciso ter ID único para cada I importação de arquivo em meu programa :)
James P.
7

Como gabuzo disse, às vezes eu uso AtomicIntegers quando quero passar um int por referência. É uma classe interna que possui código específico da arquitetura, por isso é mais fácil e provavelmente mais otimizado do que qualquer MutableInteger que eu poderia codificar rapidamente. Dito isto, parece um abuso da classe.

David Ehrmann
fonte
7

No Java 8, as classes atômicas foram estendidas com duas funções interessantes:

  • int getAndUpdate (IntUnaryOperator updateFunction)
  • int updateAndGet (IntUnaryOperator updateFunction)

Ambos estão usando o updateFunction para executar a atualização do valor atômico. A diferença é que o primeiro retorna o valor antigo e o segundo retorna o novo valor. O updateFunction pode ser implementado para executar operações mais complexas de "comparação e configuração" do que a operação padrão. Por exemplo, ele pode verificar se o contador atômico não fica abaixo de zero, normalmente exigiria sincronização e aqui o código é livre de bloqueios:

    public class Counter {

      private final AtomicInteger number;

      public Counter(int number) {
        this.number = new AtomicInteger(number);
      }

      /** @return true if still can decrease */
      public boolean dec() {
        // updateAndGet(fn) executed atomically:
        return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
      }
    }

O código é obtido do Java Atomic Example .

pwojnowski
fonte
5

Normalmente, uso o AtomicInteger quando preciso fornecer IDs para objetos que podem ser acessados ​​ou criados a partir de vários threads, e geralmente o uso como um atributo estático na classe que eu acesso no construtor dos objetos.

DguezTorresEmmanuel
fonte
4

Você pode implementar bloqueios sem bloqueio usando compareAndSwap (CAS) em números inteiros atômicos ou longos. O documento Memória transacional do software "Tl2" descreve isso:

Associamos um bloqueio de gravação com versão especial a todos os locais de memória transacionados. Em sua forma mais simples, o bloqueio de gravação com versão é um spinlock de palavra única que usa uma operação CAS para adquirir o bloqueio e um armazenamento para liberá-lo. Como é necessário apenas um bit para indicar que o bloqueio foi efetuado, usamos o restante da palavra de bloqueio para armazenar um número de versão.

O que está descrevendo é primeiro ler o número inteiro atômico. Divida isso em um bit de bloqueio ignorado e o número da versão. Tente gravar o CAS como o bit de bloqueio limpo com o número da versão atual no conjunto de bits de bloqueio e o próximo número da versão. Faça um loop até ter sucesso e você seja o segmento que possui o bloqueio. Desbloqueie definindo o número da versão atual com o bit de bloqueio limpo. O documento descreve o uso dos números de versão nos bloqueios para coordenar que os segmentos tenham um conjunto consistente de leituras quando escrevem.

Este artigo descreve que os processadores têm suporte de hardware para operações de comparação e troca, tornando o processo muito eficiente. Alega também:

os contadores baseados em CAS sem bloqueio, usando variáveis ​​atômicas, têm melhor desempenho do que os contadores baseados em bloqueio em contenção baixa a moderada

simbo1905
fonte
3

A chave é que eles permitem acesso e modificação simultâneos com segurança. Eles são comumente usados ​​como contadores em um ambiente multithread - antes da introdução, essa deveria ser uma classe escrita pelo usuário que agrupava os vários métodos em blocos sincronizados.

Michael Berry
fonte
Entendo. É isso nos casos em que um atributo ou instância atua como uma espécie de variável global dentro de um aplicativo. Ou existem outros casos em que você pode pensar?
James P.
1

Usei o AtomicInteger para resolver o problema do jantar filósofo.

Na minha solução, as instâncias AtomicInteger foram usadas para representar os garfos, são necessárias duas por filósofo. Cada filósofo é identificado como um número inteiro, de 1 a 5. Quando um garfo é usado por um filósofo, o AtomicInteger mantém o valor do filósofo, de 1 a 5, caso contrário, o garfo não está sendo usado, portanto o valor do AtomicInteger é -1 .

O AtomicInteger permite verificar se um garfo está livre, valor == - 1, e defina-o como o proprietário do garfo, se estiver livre, em uma operação atômica. Veja o código abaixo.

AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){    
    if (Hungry) {
        //if fork is free (==-1) then grab it by denoting who took it
        if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
          //at least one fork was not succesfully grabbed, release both and try again later
            fork0.compareAndSet(p, -1);
            fork1.compareAndSet(p, -1);
            try {
                synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork                    
                    lock.wait();//try again later, goes back up the loop
                }
            } catch (InterruptedException e) {}

        } else {
            //sucessfully grabbed both forks
            transition(fork_l_free_and_fork_r_free);
        }
    }
}

Como o método compareAndSet não bloqueia, ele deve aumentar a taxa de transferência, mais trabalho realizado. Como você deve saber, o problema do Dining Philosophers é usado quando o acesso controlado aos recursos é necessário, ou seja, garfos, como um processo precisa de recursos para continuar trabalhando.

Rodrigo Gomez
fonte
0

Exemplo simples para a função compareAndSet ():

import java.util.concurrent.atomic.AtomicInteger; 

public class GFG { 
    public static void main(String args[]) 
    { 

        // Initially value as 0 
        AtomicInteger val = new AtomicInteger(0); 

        // Prints the updated value 
        System.out.println("Previous value: "
                           + val); 

        // Checks if previous value was 0 
        // and then updates it 
        boolean res = val.compareAndSet(0, 6); 

        // Checks if the value was updated. 
        if (res) 
            System.out.println("The value was"
                               + " updated and it is "
                           + val); 
        else
            System.out.println("The value was "
                               + "not updated"); 
      } 
  } 

O impresso é: valor anterior: 0 O valor foi atualizado e é 6 Outro exemplo simples:

    import java.util.concurrent.atomic.AtomicInteger; 

public class GFG { 
    public static void main(String args[]) 
    { 

        // Initially value as 0 
        AtomicInteger val 
            = new AtomicInteger(0); 

        // Prints the updated value 
        System.out.println("Previous value: "
                           + val); 

         // Checks if previous value was 0 
        // and then updates it 
        boolean res = val.compareAndSet(10, 6); 

          // Checks if the value was updated. 
          if (res) 
            System.out.println("The value was"
                               + " updated and it is "
                               + val); 
        else
            System.out.println("The value was "
                               + "not updated"); 
    } 
} 

O impresso é: Valor anterior: 0 O valor não foi atualizado

M Shafaei N
fonte