Buscar..


Introducción

Los tipos atómicos de Java son tipos mutables simples que proporcionan operaciones básicas que son seguras para subprocesos y atómicas sin tener que recurrir al bloqueo. Están diseñados para usarse en casos en que el bloqueo sería un cuello de botella concurrente, o donde existe riesgo de bloqueo o bloqueo.

Parámetros

Parámetro Descripción
conjunto Conjunto volátil del campo.
obtener Lectura volátil del campo.
lazySet Esta es una operación ordenada por la tienda del campo.
compareAndSet Si el valor es el valor de expiración, entonces envíelo al nuevo valor
getAndSet obtener el valor actual y actualizar

Observaciones

Muchos en esencialmente combinaciones de lecturas o escrituras volátiles y operaciones CAS . La mejor manera de entender esto es mirar el código fuente directamente. Por ejemplo , AtomicInteger , Unsafe.getAndSet

Creando Tipos Atómicos

Para código simple de múltiples hilos, el uso de la sincronización es aceptable. Sin embargo, el uso de la sincronización tiene un impacto de vida, y a medida que la base de código se vuelve más compleja, aumenta la probabilidad de que termine con un punto muerto , un hambre o un bloqueo de vida .

En casos de concurrencia más compleja, el uso de variables atómicas suele ser una mejor alternativa, ya que permite acceder a una variable individual de una manera segura para subprocesos sin la sobrecarga de usar métodos sincronizados o bloques de código.

Creando un tipo AtomicInteger :

AtomicInteger aInt = new AtomicInteger() // Create with default value 0

AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1

Del mismo modo para otros tipos de instancia.

AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array

Del mismo modo para otros tipos atómicos.

Hay una notable excepción de que no hay tipos float y double . Estos se pueden simular mediante el uso de Float.floatToIntBits(float) y Float.intBitsToFloat(int) para float , así como Double.doubleToLongBits(double) y Double.longBitsToDouble(long) para dobles.

Si está dispuesto a usar sun.misc.Unsafe , puede usar cualquier variable primitiva como atómica utilizando la operación atómica en sun.misc.Unsafe . Todos los tipos primitivos deben convertirse o codificarse en int o longs para usarlos de esta manera. Para más sobre esto vea: sun.misc.Unsafe .

Motivación para los tipos atómicos

La forma sencilla de implementar aplicaciones de subprocesos múltiples es utilizar las primitivas de sincronización y bloqueo integradas de Java; por ejemplo, la palabra clave synchronized . El siguiente ejemplo muestra cómo podemos usar la synchronized para acumular cuentas.

public class Counters {
    private final int[] counters;

    public Counters(int nosCounters) {
        counters = new int[nosCounters];
    }

    /**
     * Increments the integer at the given index
     */
    public synchronized void count(int number) {
        if (number >= 0 && number < counters.length) {
            counters[number]++;
        }
    }

    /**
     * Obtains the current count of the number at the given index,
     * or if there is no number at that index, returns 0.
     */
    public synchronized int getCount(int number) {
        return (number >= 0 && number < counters.length) ? counters[number] : 0;
    }
}

Esta implementación funcionará correctamente. Sin embargo, si tiene un gran número de subprocesos que realizan muchas llamadas simultáneas en el mismo objeto de Counters , la sincronización puede ser un cuello de botella. Específicamente:

  1. Cada llamada de método synchronized comenzará con el subproceso actual adquiriendo el bloqueo para la instancia de Counters .
  2. El hilo mantendrá el bloqueo mientras verifica el valor del number y actualiza el contador.
  3. Finalmente, el desbloqueará el bloqueo, permitiendo el acceso de otros hilos.

Si un hilo intenta adquirir el bloqueo mientras otro lo retiene, el hilo que intenta iniciarse se bloqueará (detendrá) en el paso 1 hasta que se libere el bloqueo. Si hay varios hilos en espera, uno de ellos lo obtendrá y los otros continuarán bloqueados.

Esto puede llevar a un par de problemas:

  • Si hay mucha disputa por el bloqueo (es decir, un montón de subprocesos intenta adquirirlo), entonces se pueden bloquear algunos subprocesos durante mucho tiempo.

  • Cuando se bloquea un subproceso esperando el bloqueo, el sistema operativo normalmente intentará cambiar la ejecución a un subproceso diferente. Este cambio de contexto incurre en un impacto de rendimiento relativamente grande en el procesador.

  • Cuando hay varios subprocesos bloqueados en el mismo bloqueo, no hay ninguna garantía de que ninguno de ellos se trate de manera "justa" (es decir, se garantiza que cada subproceso está programado para ejecutarse). Esto puede conducir a la hambruna de hilos .

¿Cómo se implementan los tipos atómicos?

Comencemos reescribiendo el ejemplo anterior usando contadores de AtomicInteger :

public class Counters {
    private final AtomicInteger[] counters;

    public Counters(int nosCounters) {
        counters = new AtomicInteger[nosCounters];
        for (int i = 0; i < nosCounters; i++) {
            counters[i] = new AtomicInteger();
        }
    }

    /**
     * Increments the integer at the given index
     */
    public void count(int number) {
        if (number >= 0 && number < counters.length) {
            counters[number].incrementAndGet();
        }
    }

    /**
     * Obtains the current count of the object at the given index,
     * or if there is no number at that index, returns 0.
     */
    public int getCount(int number) {
        return (number >= 0 && number < counters.length) ? 
                counters[number].get() : 0;
    }
}

Hemos reemplazado el int[] con un AtomicInteger[] , y lo AtomicInteger[] inicializado con una instancia en cada elemento. También hemos agregado llamadas a incrementAndGet() y get() en lugar de operaciones en valores int .

Pero lo más importante es que podemos eliminar la palabra clave synchronized porque ya no se requiere el bloqueo. Esto funciona porque las operaciones incrementAndGet() y get() son atómicas y seguras para subprocesos . En este contexto, significa que:

  • Cada contador en la matriz solo será observable en el estado "antes" de una operación (como un "incremento") o en el estado "después".

  • Suponiendo que la operación ocurra en el tiempo T , ningún hilo podrá ver el estado "antes" después del tiempo T

Además, mientras que dos subprocesos podrían intentar actualizar la misma instancia de AtomicInteger al mismo tiempo, las implementaciones de las operaciones aseguran que solo se produzca un incremento a la vez en la instancia dada. Esto se hace sin bloqueo, lo que a menudo resulta en un mejor rendimiento.

¿Cómo funcionan los tipos atómicos?

Los tipos atómicos generalmente se basan en instrucciones de hardware especializadas en el conjunto de instrucciones de la máquina de destino. Por ejemplo, los conjuntos de instrucciones basadas en Intel proporcionan una instrucción CAS ( Comparar e Intercambiar ) que realizará una secuencia específica de operaciones de memoria de forma atómica.

Estas instrucciones de bajo nivel se utilizan para implementar operaciones de alto nivel en las API de las respectivas clases AtomicXxx . Por ejemplo, (de nuevo, en pseudocódigo tipo C):

private volatile num;

int increment() {
  while (TRUE) {
    int old = num;
    int new = old + 1;
    if (old == compare_and_swap(&num, old, new)) {
      return new;
    }
  }
}

Si no hay contención en el AtomicXxxx , la prueba if tendrá éxito y el bucle finalizará inmediatamente. Si hay contención, entonces el if fallará para todos menos uno de los subprocesos, y "girarán" en el bucle durante un pequeño número de ciclos del bucle. En la práctica, el giro es más rápido en órdenes de magnitud (excepto en niveles de contención irrealmente altos , donde la sincronización funciona mejor que las clases atómicas porque cuando falla la operación CAS, el reintento solo agregará más contención) que suspender el hilo y cambiar a otro uno.

Incidentalmente, las instrucciones CAS suelen ser utilizadas por la JVM para implementar el bloqueo no controlado . Si la JVM puede ver que un bloqueo no está bloqueado actualmente, intentará usar un CAS para adquirir el bloqueo. Si el CAS tiene éxito, entonces no hay necesidad de realizar la costosa programación de subprocesos, el cambio de contexto, etc. Para obtener más información sobre las técnicas utilizadas, consulte Bloqueo sesgado en HotSpot .



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow