Respuestas:
Hay dos usos principales de AtomicInteger
:
Como un contador atómico ( incrementAndGet()
, etc.) que puede ser usado por muchos hilos simultáneamente
Como una primitiva que admite la instrucción de comparar e intercambiar ( compareAndSet()
) para implementar algoritmos sin bloqueo.
Aquí hay un ejemplo de generador de números aleatorios sin bloqueo de Java Concurrency In Practice 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 puede ver, básicamente funciona casi de la misma manera que incrementAndGet()
, pero realiza un cálculo arbitrario ( calculateNext()
) en lugar de un incremento (y procesa el resultado antes del retorno).
read
y write that value + 1
, esto se detecta en lugar de sobrescribir la actualización anterior (evitando el problema de "actualización perdida"). Este es realmente un caso especial de compareAndSet
- si el valor anterior era 2
, la clase realmente llama compareAndSet(2, 3)
- entonces si otro hilo ha modificado el valor mientras tanto, el método de incremento se reinicia efectivamente desde el principio.
El ejemplo más simple que se me ocurre es incrementar la operación atómica.
Con entradas estándar:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
Con AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
Esta última es una forma muy simple de realizar efectos de mutaciones simples (especialmente el conteo o la indexación única), sin tener que recurrir a la sincronización de todos los accesos.
Se puede emplear una lógica más compleja sin sincronización mediante el uso compareAndSet()
como un tipo de bloqueo optimista: obtenga el valor actual, calcule el resultado en función de esto, establezca este resultado si el valor sigue siendo la entrada utilizada para hacer el cálculo, o comience de nuevo, pero el los ejemplos de conteo son muy útiles, y a menudo los utilizo AtomicIntegers
para contar y generadores únicos para toda la VM si hay alguna pista de que hay varios subprocesos involucrados, porque son tan fáciles de trabajar que casi consideraría una optimización prematura el uso simple ints
.
Si bien casi siempre puede lograr las mismas garantías de sincronización ints
y las synchronized
declaraciones apropiadas , lo mejor de esto AtomicInteger
es que la seguridad de los hilos está integrada en el objeto real, en lugar de tener que preocuparse por las posibles entrelazamientos y monitores que se mantienen, de cada método eso sucede para acceder al int
valor. Es mucho más difícil violar accidentalmente la seguridad de subprocesos al llamar getAndIncrement()
que al regresar i++
y recordar (o no) adquirir de antemano el conjunto correcto de monitores.
Si observa los métodos que AtomicInteger tiene, notará que tienden a corresponder a operaciones comunes en ints. Por ejemplo:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
es la versión segura de este subproceso:
static int i;
// Later, in a thread
int current = ++i;
El mapa de métodos
++i
es el siguiente : is i.incrementAndGet()
i++
is i.getAndIncrement()
--i
is i.decrementAndGet()
i--
is i.getAndDecrement()
i = x
is i.set(x)
x = i
is isx = i.get()
También hay otros métodos de conveniencia, como compareAndSet
oaddAndGet
El uso principal de AtomicInteger
es cuando se encuentra en un contexto multiproceso y necesita realizar operaciones seguras de subprocesos en un entero sin usar synchronized
. La asignación y recuperación en el tipo primitivo int
ya son atómicas, pero AtomicInteger
vienen con muchas operaciones que no son atómicas int
.
Los más simples son los getAndXXX
o xXXAndGet
. Por ejemplo, getAndIncrement()
es un equivalente atómico al i++
que no es atómico porque en realidad es un atajo para tres operaciones: recuperación, adición y asignación. compareAndSet
es muy útil para implementar semáforos, cerraduras, pestillos, etc.
Usar el AtomicInteger
es más rápido y más legible que realizarlo usando la sincronización.
Una prueba simple:
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));
}
En mi PC con Java 1.6, la prueba atómica se ejecuta en 3 segundos mientras que la sincronizada se ejecuta en aproximadamente 5,5 segundos. El problema aquí es que la operación para sincronizar ( notAtomic++
) es realmente corta. Por lo tanto, el costo de la sincronización es realmente importante en comparación con la operación.
Además de atomicidad, AtomicInteger se puede usar como una versión mutable de, Integer
por ejemplo, en Map
s como valores.
AtomicInteger
como clave de mapa, porque usa la equals()
implementación predeterminada , que seguramente no es lo que esperarías de la semántica si se usara en un mapa.
Por ejemplo, tengo una biblioteca que genera instancias de alguna clase. Cada una de estas instancias debe tener una ID entera única, ya que estas instancias representan comandos que se envían a un servidor, y cada comando debe tener una ID única. Dado que varios subprocesos pueden enviar comandos simultáneamente, utilizo un AtomicInteger para generar esas ID. Un enfoque alternativo sería usar algún tipo de bloqueo y un número entero regular, pero eso es más lento y menos elegante.
Como dijo gabuzo, a veces uso AtomicIntegers cuando quiero pasar un int por referencia. Es una clase incorporada que tiene un código específico de arquitectura, por lo que es más fácil y probablemente más optimizado que cualquier MutableInteger que pueda codificar rápidamente. Dicho eso, se siente como un abuso de la clase.
En Java 8, las clases atómicas se han ampliado con dos funciones interesantes:
Ambos están utilizando la función updateFunction para realizar la actualización del valor atómico. La diferencia es que el primero devuelve el valor anterior y el segundo devuelve el valor nuevo. La función updateFunction se puede implementar para realizar operaciones de "comparación y configuración" más complejas que la estándar. Por ejemplo, puede verificar que el contador atómico no descienda por debajo de cero, normalmente requeriría sincronización, y aquí el código no tiene bloqueo:
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;
}
}
El código está tomado de Java Atomic Example .
Usualmente uso AtomicInteger cuando necesito dar ID a objetos a los que se puede acceder o crear desde múltiples hilos, y generalmente lo uso como un atributo estático en la clase a la que accedo en el constructor de los objetos.
Puede implementar bloqueos sin bloqueo utilizando compareAndSwap (CAS) en enteros atómicos o longs. El documento "Tl2" Software Transactional Memory describe esto:
Asociamos un bloqueo de escritura versionado especial con cada ubicación de memoria transaccionada. En su forma más simple, el bloqueo de escritura versionado es un spinlock de una sola palabra que utiliza una operación CAS para adquirir el bloqueo y una tienda para liberarlo. Como solo se necesita un bit para indicar que se ha tomado el bloqueo, usamos el resto de la palabra de bloqueo para contener un número de versión.
Lo que está describiendo es leer primero el entero atómico. Divida esto en un bit de bloqueo ignorado y el número de versión. Intente escribir CAS como el bit de bloqueo borrado con el número de versión actual en el conjunto de bits de bloqueo y el siguiente número de versión. Haz un bucle hasta que tengas éxito y seas el hilo propietario de la cerradura. Desbloquee configurando el número de versión actual con el bit de bloqueo desactivado. El documento describe el uso de los números de versión en los bloqueos para coordinar que los hilos tengan un conjunto consistente de lecturas cuando escriben.
Este artículo describe que los procesadores tienen soporte de hardware para operaciones de comparación e intercambio, lo que hace que sea muy eficiente. También afirma:
Los contadores basados en CAS sin bloqueo que utilizan variables atómicas tienen un mejor rendimiento que los contadores basados en bloqueo en contención baja a moderada
La clave es que permiten el acceso concurrente y la modificación de forma segura. Se usan comúnmente como contadores en un entorno multiproceso; antes de su introducción, tenía que ser una clase escrita por el usuario que envolviera los diversos métodos en bloques sincronizados.
Usé AtomicInteger para resolver el problema del Dining Philosopher.
En mi solución, se utilizaron instancias AtomicInteger para representar los tenedores, se necesitan dos por filósofo. Cada filósofo se identifica como un número entero, del 1 al 5. Cuando un filósofo usa una bifurcación, el AtomicInteger tiene el valor del filósofo, del 1 al 5; de lo contrario, la bifurcación no se usa, por lo que el valor del AtomicInteger es -1 .
El AtomicInteger permite verificar si una bifurcación está libre, valor == - 1, y establecerla en el propietario de la bifurcación si está libre, en una operación atómica. Ver el código a continuación.
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);
}
}
}
Debido a que el método compareAndSet no bloquea, debería aumentar el rendimiento, más trabajo realizado. Como ya sabrá, el problema de Dining Philosophers se usa cuando se necesita acceso controlado a los recursos, es decir, se necesitan tenedores, como un proceso necesita recursos para continuar trabajando.
Ejemplo simple para la función 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");
}
}
El impreso es: valor anterior: 0 El valor se actualizó y es 6 Otro ejemplo simple:
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");
}
}
El impreso es: Valor anterior: 0 El valor no se actualizó