java - example - Usos prácticos de AtomicInteger.
atomicinteger java (10)
Entiendo que AtomicInteger y otras variables atómicas permiten accesos concurrentes. ¿En qué casos se utiliza esta clase normalmente?
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 la arquitectura, por lo que es más fácil y probablemente más optimizado que cualquier MutableInteger que pudiera codificar rápidamente. Dicho esto, se siente como un abuso de la clase.
El ejemplo más simple que se me ocurre es hacer un incremento de 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 sencilla 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 sin sincronización más compleja utilizando compareAndSet()
como un tipo de bloqueo optimista: obtenga el valor actual, calcule el resultado en base a esto, establezca este resultado si el valor sigue siendo la entrada utilizada para hacer el cálculo, o comience de nuevo - pero los ejemplos de conteo son muy útiles, y a menudo AtomicIntegers
para el conteo y generadores únicos en toda la máquina virtual si hay algún indicio de múltiples hilos involucrados, porque son tan fáciles de trabajar que casi lo considero prematuro Optimización para utilizar ints
simples.
Si bien casi siempre se pueden lograr las mismas garantías de sincronización con los ints
y las declaraciones synchronized
adecuadas, la belleza de AtomicInteger
es que la seguridad de subprocesos está incorporada en el objeto real en sí, en lugar de que usted deba preocuparse por los posibles intrusiones y monitores mantenidos. De cada método que pasa para acceder al valor int
. Es mucho más difícil violar accidentalmente la seguridad de subprocesos cuando se llama a getAndIncrement()
que cuando se devuelve i++
y se recuerda (o no) para adquirir el conjunto correcto de monitores de antemano.
El uso principal de AtomicInteger
es cuando estás en un contexto multiproceso y necesitas 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
viene con muchas operaciones que no son atómicas en int
.
Los más simples son los getAndXXX
o xXXAndGet
. Por ejemplo, getAndIncrement()
es un equivalente atómico a 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, bloqueos, cierres, etc.
El uso de AtomicInteger
es más rápido y más legible que hacer lo mismo 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 de sincronización ( 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 la atomicidad, AtomicInteger puede usarse como una versión mutable de Integer
por ejemplo, en Map
s como valores.
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 updateFunction para realizar la actualización del valor atómico. La diferencia es que el primero devuelve el valor antiguo y el segundo devuelve el nuevo valor. El 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 esté por debajo de cero, normalmente requeriría sincronización, y aquí el código está libre de 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 .
La clave es que permiten el acceso concurrente y la modificación de forma segura. Se usan comúnmente como contadores en un entorno de multiproceso: antes de su introducción, esto tenía que ser una clase escrita por el usuario que envolvía los diversos métodos en bloques sincronizados.
Normalmente uso AtomicInteger cuando necesito dar identificadores a objetos a los que se puede acceder o crear desde múltiples subprocesos, y generalmente lo uso como un atributo estático en la clase a la que accedo en el constructor de los objetos.
Por ejemplo, tengo una biblioteca que genera instancias de alguna clase. Cada una de estas instancias debe tener un ID entero único, ya que estas instancias representan comandos que se envían a un servidor, y cada comando debe tener una ID única. Dado que se permite que varios subprocesos envíen comandos simultáneamente, utilizo un AtomicInteger para generar esos ID. Un enfoque alternativo sería utilizar algún tipo de bloqueo y un entero normal, pero eso es más lento y menos elegante.
Puede implementar bloqueos no bloqueantes utilizando compareAndSwap (CAS) en enteros atómicos o largos. El papel de la memoria transaccional del software "Tl2" describe esto:
Asociamos un bloqueo de escritura con una versión especial a cada ubicación de memoria negociada. 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 realizado el bloqueo, utilizamos el resto de la palabra de bloqueo para mantener un número de versión.
Lo que está describiendo es primero leer el entero atómico. Divida esto en un bit de bloqueo ignorado y el número de versión. Intente escribirlo como el bit de bloqueo borrado con el número de versión actual al conjunto de bits de bloqueo y el siguiente número de versión. Bucle hasta que tenga éxito y usted es el hilo que posee el bloqueo. 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 lo hace 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 contienda de baja a moderada
Si observa los métodos que tiene AtomicInteger, 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 subprocesos de esto:
static int i;
// Later, in a thread
int current = ++i;
Los métodos se mapean así:
++i
is 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
Hay dos usos principales de AtomicInteger
:
Como un contador atómico (
incrementAndGet()
, etc.) que pueden ser utilizados por muchos hilos simultáneamenteComo una primitiva que admite la instrucción de compare-and-swap (
compareAndSet()
) para implementar algoritmos de no bloqueo.Este es un ejemplo de un generador de números aleatorios no bloqueante de la Concurrencia de Java en Práctica 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 forma que
incrementAndGet()
, pero realiza un cálculo arbitrario (calculateNext()
) en lugar de incremento (y procesa el resultado antes de la devolución).