CodeGym /Cursos /JAVA 25 SELF /AtomicInteger, AtomicReference: operaciones atómicas

AtomicInteger, AtomicReference: operaciones atómicas

JAVA 25 SELF
Nivel 53 , Lección 3
Disponible

1. ¿Por qué i++ no funciona en multihilo?

Empecemos con el caso clásico: tenemos una variable‑contador, por ejemplo, la cantidad de solicitudes procesadas o el número de archivos descargados. Queremos que varios hilos incrementen este contador. ¿Qué puede salir mal si simplemente escribimos i++?

Ejemplo: condición de carrera en el incremento

public class Counter {
    public int count = 0;

    public void increment() {
        count++; // ¡No es atómico!
    }
}

Imaginemos que dos hilos llaman a increment() a la vez. Ambos leen el valor antiguo, ambos lo incrementan en 1, y ambos escriben… ¡el mismo valor nuevo! Como resultado, uno de los incrementos «se pierde». Si esto se repite muchas veces, el valor final será menor de lo esperado.

¿Por qué ocurre esto?
La operación i++ en realidad consta de tres pasos:

  1. Leer el valor de la variable (por ejemplo, 5).
  2. Incrementar ese valor en 1.
  3. Escribir el nuevo valor de vuelta en memoria.

Y en un entorno multihilo otro hilo puede alcanzar a modificar la variable entre estos pasos. Resultado: «condición de carrera» (race condition).

¿Qué son las operaciones atómicas?

Una operación atómica es una acción que o bien se ejecuta completamente, o no se ejecuta en absoluto, y ningún otro hilo puede «interponerse» en mitad de esa operación.

En Java hay un conjunto de clases que proporcionan este tipo de operaciones para primitivos y referencias. Están ubicadas en el paquete java.util.concurrent.atomic. Las más populares:

  • AtomicInteger — tipo entero atómico.
  • AtomicLong — atómico para long.
  • AtomicBoolean — atómico para boolean.
  • AtomicReference<T> — referencia atómica a un objeto de cualquier tipo.

2. AtomicInteger: contador seguro para hilos

Declaración y uso básico

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet(); // Incremento atómico
    }

    public int get() {
        return count.get();
    }
}

Aquí incrementAndGet() realiza «incrementar y devolver el nuevo valor» como una única operación indivisible. Incluso si 100 hilos llaman a este método simultáneamente, no se perderá ningún incremento.

Métodos útiles:

Método Descripción
get()
Obtener el valor actual
set(int value)
Establecer el valor
incrementAndGet()
Incrementar en 1 y devolver el nuevo valor
getAndIncrement()
Devolver el valor actual y luego incrementarlo en 1
addAndGet(int delta)
Incrementar en delta y devolver el nuevo valor
compareAndSet(expect, update)
Si el valor actual es igual a expect, establecer update (CAS)

Ejemplo: contador multihilo

Supongamos que tenemos una clase que cuenta el número de mensajes procesados en un chat.

public class MessageStatistics {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void onMessageReceived() {
        int newCount = messageCount.incrementAndGet();
        System.out.println("Total de mensajes: " + newCount);
    }

    public int getMessageCount() {
        return messageCount.get();
    }
}

Por dentro: ¿cómo funciona AtomicInteger?

Internamente, AtomicInteger usa una instrucción especial del procesador — CAS (Compare-And-Swap, «comparar‑y‑cambiar»). Es una operación atómica que compara el valor actual de la variable con el esperado y, si coinciden, escribe el nuevo valor. Si otro hilo ha conseguido cambiar la variable — la operación no se realiza y se vuelve a intentar.

Esquema de funcionamiento:

1. Leemos el valor actual (por ejemplo, 5)
2. Comparamos con el esperado (5)
3. Si coincide — escribimos el nuevo valor (6)
4. Si no coincide — repetimos el intento

Todo esto ocurre muy rápido y sin bloqueos (lock‑free). Por eso las clases atómicas suelen ser más rápidas que synchronized, especialmente con una gran cantidad de hilos.

3. AtomicReference: referencia atómica a un objeto

AtomicReference<T> es un contenedor atómico genérico para cualquier objeto. Permite cambiar de forma segura la referencia a un objeto desde distintos hilos.

Ejemplo: actualización de la referencia segura para hilos

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceExample {
    private final AtomicReference<String> latestMessage = new AtomicReference<>("");

    public void updateMessage(String message) {
        latestMessage.set(message);
    }

    public String getLatestMessage() {
        return latestMessage.get();
    }
}

Uso de compareAndSet

La operación más interesante es compareAndSet(expected, newValue). Permite actualizar el valor solo si no ha cambiado desde la última lectura.

public void safeUpdate(String oldValue, String newValue) {
    boolean success = latestMessage.compareAndSet(oldValue, newValue);
    if (success) {
        System.out.println("¡Actualización realizada con éxito!");
    } else {
        System.out.println("Alguien ya ha cambiado el valor, inténtalo de nuevo.");
    }
}

Esta es la base de los algoritmos no bloqueantes: desde colas y pilas hasta cachés, donde es importante evitar bloqueos innecesarios.

4. Ejemplos de uso en una aplicación

Ejemplo 1: contador de mensajes multihilo

public class ChatRoom {
    private final AtomicInteger messageCount = new AtomicInteger(0);

    public void receiveMessage(String message) {
        // ... procesamiento del mensaje ...
        int count = messageCount.incrementAndGet();
        System.out.println("Nuevo mensaje: " + message + ". Total de mensajes: " + count);
    }
}

Ejemplo 2: actualización segura de la referencia al último mensaje

public class ChatRoom {
    private final AtomicReference<String> lastMessage = new AtomicReference<>("");

    public void receiveMessage(String message) {
        lastMessage.set(message);
        // ... procesamiento ...
    }

    public String getLastMessage() {
        return lastMessage.get();
    }
}

Si necesitas actualizar la referencia solo si el último mensaje no ha cambiado (para evitar «pérdidas» con actualizaciones simultáneas), utiliza compareAndSet.

5. Limitaciones y escollos

¿Cuándo las clases atómicas no son la panacea?

Las variables atómicas son perfectas para operaciones sencillas: incremento, establecimiento de valor, comprobación y sustitución. Pero si necesitas actualizar varias variables a la vez, la atomicidad ya no está garantizada. Por ejemplo, si tienes dos contadores y quieres incrementar ambos como una sola operación — aquí necesitas synchronized u otro mecanismo de sincronización.

Ejemplo de uso incorrecto

// ¡No es atómico!
if (ref.get() == null) {
    ref.set("Hello");
}

Entre get() y set(...) otro hilo puede cambiar el valor, y la condición dejaría de ser cierta. Para estos casos utiliza compareAndSet.

Clases atómicas ≠ objetos seguros para hilos

Si el objeto al que apunta AtomicReference no es en sí mismo seguro para hilos, el reemplazo de la referencia será atómico, pero la modificación de los campos del objeto no lo será. Por ejemplo, si guardas en AtomicReference<List<String>> un ArrayList normal, la propia lista no se vuelve thread‑safe.

6. Clases atómicas avanzadas

En el paquete java.util.concurrent.atomic hay otras clases útiles:

  • AtomicLong, AtomicBoolean — para long y boolean.
  • AtomicIntegerArray, AtomicReferenceArray — operaciones atómicas sobre arrays.
  • LongAdder, LongAccumulator — para contadores de alta concurrencia.

LongAdder y LongAccumulator

Si tienes muchísimos hilos y un AtomicInteger normal se convierte en un «cuello de botella» (todos los hilos compiten por una sola variable), utiliza LongAdder. Divide el contador en varias celdas internas y las suma al solicitar el valor, lo que ofrece ventaja con alta contención.

import java.util.concurrent.atomic.LongAdder;

public class FastCounter {
    private final LongAdder adder = new LongAdder();

    public void increment() {
        adder.increment();
    }

    public long getCount() {
        return adder.sum();
    }
}

7. Errores típicos al trabajar con variables atómicas

Error n.º 1: esperar la atomicidad de operaciones complejas.
Si necesitas realizar varias acciones sobre un valor, las clases atómicas no te salvarán — entre pasos otro hilo puede cambiar los datos. Para operaciones compuestas utiliza compareAndSet o sincronización.

Error n.º 2: ignorar la seguridad para hilos de los objetos anidados.
Si en AtomicReference hay un objeto normal, sus métodos y campos no se vuelven seguros para hilos. Solo es atómico el reemplazo de la referencia.

Error n.º 3: usar clases atómicas sin necesidad.
En código monohilo los tipos atómicos son redundantes y un poco más lentos que las variables normales debido a comprobaciones adicionales.

Error n.º 4: optimización prematura.
A veces es más simple y fiable usar synchronized, especialmente cuando la lógica es compleja y afecta a varias variables a la vez. No siempre compensa construir soluciones sin bloqueos.

Error n.º 5: olvidar el problema ABA.
Un caso raro pero importante: el valor cambia de A a B y vuelve a A — compareAndSet «cree» que no ha cambiado nada. Para tales escenarios utiliza clases especiales como AtomicStampedReference (o AtomicMarkableReference).

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION