Respostas:
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).
read
e 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.
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 AtomicIntegers
para 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 ints
e synchronized
declaraçõ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 int
valor. É 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.
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:
++i
is i.incrementAndGet()
i++
is i.getAndIncrement()
--i
is i.decrementAndGet()
i--
is i.getAndDecrement()
i = x
is i.set(x)
x = i
isx = i.get()
Existem outros métodos de conveniência, como compareAndSet
ouaddAndGet
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 int
já são atômicas, mas AtomicInteger
vêm com muitas operações que não são atômicas int
.
Os mais simples são os getAndXXX
ou 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, Integer
por exemplo, em Map
s como valores.
AtomicInteger
como 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.
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.
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.
No Java 8, as classes atômicas foram estendidas com duas funções interessantes:
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 .
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.
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
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.
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.
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