Usos prácticos para AtomicInteger

Entiendo que AtomicInteger y otras variables atómicas permiten accesos concurrentes. ¿En qué casos se usa típicamente esta clase?

Hay dos usos principales de AtomicInteger :

  • Como un contador atómico ( incrementAndGet() , etc.) que pueden ser utilizados por muchos hilos al mismo tiempo

  • Como una primitiva que admite instrucciones de comparación y cambio ( compareAndSet() ) para implementar algoritmos sin locking.

    Aquí hay un ejemplo de generador de números aleatorios sin locking 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 cálculos arbitrarios ( calculateNext() ) en lugar de incrementos (y procesa el resultado antes de devolver).

El ejemplo más simple que puedo pensar es hacer que el incremento sea una operación atómica.

Con ints 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 contar, o indexación única), sin tener que recurrir a la sincronización de todos los accesos.

Se puede emplear una lógica libre de sincronización más compleja usando compareAndSet() como un tipo de locking 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, de lo contrario comience nuevamente – pero los ejemplos de conteo son muy útiles, ya menudo AtomicIntegers para contar y generadores únicos en toda la VM si hay algún indicio de múltiples hilos implicados, porque son tan fáciles de trabajar que casi los consideraría prematuros optimización para usar ints simples.

Si bien casi siempre puede lograr las mismas garantías de sincronización con ints y declaraciones synchronized apropiadas, la belleza de AtomicInteger es que la seguridad de la hebra está integrada en el objeto real, en lugar de tener que preocuparse por los posibles entrelazados y monitores retenidos. de cada método que sucede para acceder al valor int . Es mucho más difícil violar accidentalmente la seguridad del hilo al llamar a getAndIncrement() que al devolver i++ y recordar (o no) adquirir el conjunto correcto de monitores de antemano.

Si observas los métodos que tiene AtomicInteger, notarás 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 para subprocesos de esto:

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

Los métodos se mapean así:
++i es i.incrementAndGet()
i++ es i.getAndIncrement()
--i es i.decrementAndGet()
i-- es i.getAndDecrement()
i = x es i.set(x)
x = i es x = i.get()

También hay otros métodos de conveniencia, como compareAndSet o addAndGet

El uso principal de AtomicInteger es cuando estás en un contexto multiproceso y necesitas realizar operaciones de seguridad de subprocesos en un entero sin usar synchronized . La asignación y la recuperación en el tipo primitivo int ya son atómicas, pero AtomicInteger viene con muchas operaciones que no son atómicas en int .

Los más simples son getAndXXX o xXXAndGet . Por ejemplo getAndIncrement() es un equivalente atómico de 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, lockings, pestillos, etc.

El uso de AtomicInteger es más rápido y más legible que realizar el mismo uso de la sincronización.

Una simple prueba:

 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 de sincronización (no notAtomic++ ) es realmente corta. Entonces, el costo de la sincronización es realmente importante en comparación con la operación.

Además de la atomicidad, AtomicInteger se puede usar como una versión mutable de Integer por ejemplo, en Map s como valores.

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 mandatos enviados a un servidor, y cada comando debe tener una ID única. Debido a que varios subprocesos pueden enviar comandos simultáneamente, utilizo un AtomicInteger para generar esos ID. Un enfoque alternativo sería usar algún tipo de locking y un entero regular, pero eso es más lento y menos elegante.

En Java 8, las clases atómicas se han ampliado con dos funciones interesantes:

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

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 nuevo valor. La función updateFunction puede implementarse para realizar operaciones más complejas de “comparar y configurar” que la estándar. Por ejemplo, puede verificar que el contador atómico no sea inferior a cero, normalmente requeriría sincronización, y aquí el código está libre de lockings:

  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 .

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 architecture, por lo que es más fácil y probablemente esté más optimizado que cualquier otro MutableInteger que pueda codificar rápidamente. Dicho eso, parece un abuso de la clase.

Usualmente uso AtomicInteger cuando necesito dar Ids a objetos que pueden ser accedidos o creados 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 lockings sin locking usando compareAndSwap (CAS) en enteros o largos atómicos. El documento de memoria transaccional de software “Tl2” describe esto:

Asociamos un locking de escritura versionado especial con cada ubicación de memoria transaccionada. En su forma más simple, el locking de escritura versionado es un spinlock de palabra única que usa una operación CAS para adquirir el locking y una tienda para liberarlo. Como solo se necesita un bit para indicar que se ha realizado el locking, usamos el rest de la palabra de locking para mantener un número de versión.

Lo que está describiendo es leer primero el número entero atómico. Dividir esto en un bit de locking ignorado y el número de versión. Intento de CAS escribirlo como el bit de locking borrado con el número de versión actual para el conjunto de locking de bits y el siguiente número de versión. Continúa hasta que tengas éxito y tu eres el hilo que posee el locking. Desbloquee configurando el número de versión actual con el bit de locking borrado. El documento describe el uso de los números de versión en los lockings 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 y cambio, lo que lo hace muy eficiente. También afirma:

Los contadores no bloqueantes basados ​​en CAS que usan variables atómicas tienen un mejor rendimiento que los contadores basados ​​en lockings en contención de 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, esta tenía que ser una clase escrita por el usuario que completaba los diversos métodos en bloques sincronizados.