Usos prácticos para AtomicInteger

230

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

James P.
fuente

Respuestas:

190

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).

axtavt
fuente
1
Creo que entiendo el primer uso. Esto es para asegurarse de que el contador se haya incrementado antes de que se vuelva a acceder a un atributo. ¿Correcto? ¿Podría dar un breve ejemplo para el segundo uso?
James P.
8
Su comprensión del primer uso es bastante cierta: simplemente garantiza que si otro hilo modifica el contador entre las operaciones ready 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.
Andrzej Doyle
3
"resto> 0? resto: resto + n;" en esta expresión, ¿hay alguna razón para agregar el resto a n cuando es 0?
sandeepkunkunuru
101

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 AtomicIntegerspara 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 intsy las synchronizeddeclaraciones apropiadas , lo mejor de esto AtomicIntegeres 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 intvalor. 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.

Andrzej Doyle
fuente
2
Gracias por esta explicación clara. ¿Cuáles serían las ventajas de usar un AtomicInteger sobre una clase donde todos los métodos están sincronizados? ¿Sería considerado este último como "más pesado"?
James P.
3
Desde mi punto de vista, se trata principalmente de la encapsulación que obtienes con AtomicIntegers: la sincronización ocurre exactamente con lo que necesitas y obtienes métodos descriptivos que están en la API pública para explicar cuál es el resultado esperado. (Además, hasta cierto punto tiene razón, a menudo uno terminaría simplemente sincronizando todos los métodos en una clase que probablemente sea demasiado gruesa, aunque con HotSpot realizando optimizaciones de bloqueo y las reglas contra la optimización prematura, considero que la legibilidad es un mayor beneficio que rendimiento.)
Andrzej Doyle
Esta es una explicación muy clara y precisa, ¡Gracias!
Akash5288
Finalmente, una explicación que me lo aclaró correctamente.
Benny Bottema
58

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
++ies el siguiente : is i.incrementAndGet()
i++is i.getAndIncrement()
--iis i.decrementAndGet()
i--is i.getAndDecrement()
i = xis i.set(x)
x = iis isx = i.get()

También hay otros métodos de conveniencia, como compareAndSetoaddAndGet

Powerlord
fuente
37

El uso principal de AtomicIntegeres 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 intya son atómicas, pero AtomicIntegervienen con muchas operaciones que no son atómicas int.

Los más simples son los getAndXXXo 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. compareAndSetes muy útil para implementar semáforos, cerraduras, pestillos, etc.

Usar el AtomicIntegeres 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, Integerpor ejemplo, en Maps como valores.

gabuzo
fuente
1
No creo que me gustaría usarlo AtomicIntegercomo 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.
Andrzej Doyle
1
@Andrzej seguro, no como clave que se requiere para ser inmutable sino un valor.
gabuzo
@gabuzo ¿Alguna idea de por qué el entero atómico funciona bien sobre sincronizado?
Supun Wijerathne
La prueba es bastante antigua ahora (más de 6 años). Me podría interesar volver a hacer una prueba con un JRE reciente. No profundicé lo suficiente en el AtomicInteger para responder, pero como esta es una tarea muy específica, utilizará técnicas de sincronización que solo funcionan en este caso específico. También tenga en cuenta que la prueba es monotreada y hacer una prueba similar en un ambiente cargado de pesadez podría no dar una victoria tan clara para AtomicInteger
gabuzo
Creo que son 3 ms y 5.5 ms
Sábado
18

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.

Sergei Tachenov
fuente
Gracias por compartir este ejemplo práctico. Esto suena como algo que debería usar, ya que necesito tener una identificación única para cada archivo que importo en mi programa :)
James P.
7

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.

David Ehrmann
fuente
7

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 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 .

pwojnowski
fuente
5

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.

DguezTorresEmmanuel
fuente
4

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

simbo1905
fuente
3

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.

Michael Berry
fuente
Veo. Es esto en los casos en que un atributo o instancia actúa como una especie de variable global dentro de una aplicación. ¿O hay otros casos que se te ocurran?
James P.
1

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.

Rodrigo Gomez
fuente
0

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ó

M Shafaei N
fuente