Requisitos previos para el surgimiento de las operaciones atómicas

Echemos un vistazo a este ejemplo para ayudarlo a comprender cómo funcionan las operaciones atómicas:

public class Counter {
    int count;

    public void increment() {
        count++;
    }
}

Cuando tenemos un subproceso, todo funciona muy bien, pero si agregamos subprocesos múltiples, obtenemos resultados incorrectos, y todo porque la operación de incremento no es una operación, sino tres: una solicitud para obtener el valor actualcontar, luego increméntelo en 1 y escriba de nuevo encontar.

Y cuando dos subprocesos quieren incrementar una variable, lo más probable es que pierda datos. Es decir, ambos subprocesos reciben 100, como resultado, ambos escribirán 101 en lugar del valor esperado de 102.

¿Y como resolverlo? Necesitas usar cerraduras. La palabra clave sincronizada ayuda a resolver este problema; su uso le garantiza que un subproceso accederá al método a la vez.

public class SynchronizedCounterWithLock {
    private volatile int count;

    public synchronized void increment() {
        count++;
    }
}

Además, debe agregar la palabra clave volátil , que garantiza la visibilidad correcta de las referencias entre los hilos. Hemos revisado su trabajo arriba.

Pero todavía hay desventajas. El más grande es el rendimiento, en ese momento cuando muchos subprocesos intentan adquirir un bloqueo y uno tiene una oportunidad de escritura, el resto de los subprocesos se bloquearán o suspenderán hasta que se libere el subproceso.

Todos estos procesos, bloqueo, cambio a otro estado son muy costosos para el rendimiento del sistema.

Operaciones atómicas

El algoritmo utiliza instrucciones de máquina de bajo nivel como compare-and-swap (CAS, compare-and-swap, que garantiza la integridad de los datos y ya hay una gran cantidad de investigación al respecto).

Una operación CAS típica opera con tres operandos:

  • Espacio de memoria para el trabajo (M)
  • Valor esperado existente (A) de una variable
  • Nuevo valor (B) a configurar

CAS actualiza atómicamente M a B, pero solo si el valor de M es el mismo que el de A; de lo contrario, no se realiza ninguna acción.

En el primer y segundo caso, se devolverá el valor de M. Esto le permite combinar tres pasos, a saber, obtener el valor, comparar el valor y actualizarlo. Y todo se convierte en una sola operación a nivel de máquina.

En el momento en que una aplicación de subprocesos múltiples accede a una variable e intenta actualizarla y se aplica CAS, uno de los subprocesos la obtendrá y podrá actualizarla. Pero a diferencia de los bloqueos, otros subprocesos simplemente obtendrán errores sobre la imposibilidad de actualizar el valor. Luego pasarán a trabajar más, y el cambio está completamente excluido en este tipo de trabajo.

En este caso, la lógica se vuelve más difícil debido al hecho de que tenemos que manejar la situación en la que la operación CAS no funcionó con éxito. Simplemente modelaremos el código para que no avance hasta que la operación tenga éxito.

Introducción a los tipos atómicos

¿Se ha encontrado con una situación en la que necesita configurar la sincronización para la variable más simple de tipo int ?

La primera forma que ya hemos cubierto es usando volátil + sincronizado . Pero también hay clases especiales de Atomic*.

Si usamos CAS, las operaciones funcionan más rápido en comparación con el primer método. Y además, tenemos métodos especiales y muy convenientes para agregar un valor y operaciones de incremento y decremento.

AtomicBoolean , AtomicInteger , AtomicLong , AtomicIntegerArray , AtomicLongArray son clases en las que las operaciones son atómicas. A continuación analizaremos el trabajo con ellos.

Entero atómico

La clase AtomicInteger proporciona operaciones en un valor int que se puede leer y escribir atómicamente, además de proporcionar operaciones atómicas extendidas.

Tiene métodos get y set que funcionan como lectura y escritura de variables.

Es decir, “sucede-antes” con cualquier recepción posterior de la misma variable de la que hablábamos antes. El método atomic compareAndSet también tiene estas características de coherencia de memoria.

Todas las operaciones que devuelven un nuevo valor se realizan atómicamente:

int agregar y obtener (int delta) Añade un valor específico al valor actual.
booleano compareAndSet(int esperado, actualizar int) Establece el valor en el valor actualizado dado si el valor actual coincide con el valor esperado.
int decrementoYObtener() Disminuye el valor actual en uno.
int getAndAdd(int delta) Suma el valor dado al valor actual.
int getAndDecrement() Disminuye el valor actual en uno.
int obtener e incrementar () Aumenta el valor actual en uno.
int getAndSet(int nuevoValor) Establece el valor dado y devuelve el valor anterior.
int incrementarYObtener() Aumenta el valor actual en uno.
lazySet(int nuevoValor) Finalmente se establece en el valor dado.
booleano débilCompareAndSet (esperado, actualizar int) Establece el valor en el valor actualizado dado si el valor actual coincide con el valor esperado.

Ejemplo:

ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // prints 50