W3docs

Variables Atómicas en Java

Operaciones thread-safe sin bloqueos en Java con clases de java.util.concurrent.atomic: contadores, referencias y compare-and-set.

volatile hace que una sola lectura o una sola escritura sea thread-safe. No puede hacer que counter++ sea thread-safe — eso son tres operaciones. El paquete java.util.concurrent.atomic cubre esa brecha. Sus clases envuelven un único valor y exponen operaciones como increment-and-get y compare-and-set como instrucciones atómicas únicas — sin bloqueo, sin bloque synchronized, solo un primitivo de CPU (compare-and-swap, o CAS) que la JVM compila directamente.

Las variables atómicas son la herramienta adecuada para un número sorprendentemente grande de patrones multihilo: contadores, números de secuencia, estado tipo bandera, y cualquier idioma de "publicar un nuevo snapshot inmutable". Son más rápidas que synchronized bajo contención y dramáticamente más simples que gestionar manualmente un bloqueo alrededor de un solo campo.

La familia

El paquete tiene ocho clases de uso común:

ClaseEnvuelveOperaciones comunes
AtomicIntegerintget, set, incrementAndGet, addAndGet, compareAndSet
AtomicLonglongigual que lo anterior, sobre long
AtomicBooleanbooleanget, set, compareAndSet
AtomicReference<V>V (cualquier referencia a objeto)get, set, compareAndSet, updateAndGet
AtomicIntegerArrayint[]operaciones atómicas por índice
AtomicLongArraylong[]operaciones atómicas por índice
AtomicReferenceArray<V>V[]operaciones atómicas por índice
LongAdder / LongAccumulatorlongcontador de alta contención

Las primeras cuatro son las que usarás el 99% de las veces.

AtomicInteger — el contador correcto

El reemplazo de "volatile int más ++":

AtomicInteger counter = new AtomicInteger();          // starts at 0

counter.incrementAndGet();                            // ++counter, atomic
counter.getAndIncrement();                            // counter++, atomic
counter.addAndGet(5);                                 // counter += 5, atomic
counter.set(42);                                      // counter = 42, atomic
int n = counter.get();                                // read

counter.compareAndSet(42, 100);                       // if (counter == 42) counter = 100; return whether it changed

incrementAndGet es lo que necesitas para un contador simple; internamente es un bucle CAS que la CPU ejecuta en una instrucción en x86 moderno (LOCK XADD). Toda la operación es una sola transacción de memoria a nivel de bus — mucho más barata que adquirir un bloqueo synchronized incluso sin contención.

compareAndSet(expected, new) es el bloque de construcción para casi todo lo demás. Escribe new de forma atómica solo si el valor actual es expected, y devuelve si la escritura ocurrió. Con él puedes construir cualquier actualización atómica de un solo campo:

AtomicInteger max = new AtomicInteger(Integer.MIN_VALUE);
void recordMax(int v) {
  int cur;
  do {
    cur = max.get();
    if (v <= cur) return;                             // nothing to do
  } while (!max.compareAndSet(cur, v));               // retry if someone else updated
}

El bucle CAS es el patrón estándar: leer, calcular, intentar escribir, reintentar en caso de conflicto. Así es como se implementa incrementAndGet; así escribirías cualquier actualización compuesta sobre un solo campo.

Java 8 simplificó el bucle:

max.updateAndGet(cur -> Math.max(cur, v));            // CAS loop hidden

updateAndGet, accumulateAndGet y getAndUpdate toman una función y realizan el bucle CAS por ti. Prefiere usarlos cuando encajen.

AtomicReference<V> — la forma correcta de intercambiar un objeto

Cuando el estado compartido es más que un primitivo — un mapa de configuración, un snapshot en caché, un contenedor inmutable — AtomicReference permite intercambiar atómicamente el objeto completo:

AtomicReference<Config> currentConfig = new AtomicReference<>(initialConfig);

void reload() {
  Config c = readConfigFromDisk();                    // expensive, lock-free
  currentConfig.set(c);                               // publish atomically
}

Config get() { return currentConfig.get(); }

El truco: el contenido de Config debe ser inmutable (o no tocarse tras la publicación). El intercambio atómico publica un valor terminado; si otros hilos luego mutan los internos del valor, has perdido la seguridad. Este es el patrón de snapshot inmutable, y es la forma en que se construyen la mayoría de las cachés concurrentes, tablas de rutas y objetos de "configuración global".

updateAndGet sobre una referencia también es extremadamente útil:

AtomicReference<List<String>> log = new AtomicReference<>(List.of());

void append(String line) {
  log.updateAndGet(old -> {
    var copy = new ArrayList<>(old);
    copy.add(line);
    return List.copyOf(copy);                         // immutable snapshot
  });
}

Cada lector obtiene una lista inmutable consistente. Los escritores compiten; el bucle CAS reintenta los pocos que pierden la carrera. Barato bajo baja contención, lento pero correcto bajo alta contención.

LongAdder — el contador de alta contención

Bajo contención intensa, AtomicLong.incrementAndGet se convierte en un cuello de botella — todos los hilos golpean la misma dirección de memoria, y la CPU tiene que serializar las transacciones de bus. LongAdder resuelve esto manteniendo varios contadores internos, uno por CPU, y sumándolos al leer:

LongAdder requestCount = new LongAdder();

void onRequest() { requestCount.increment(); }        // append-only, no contention

long snapshot() { return requestCount.sum(); }        // sums every cell — not atomic but eventually consistent

Usa LongAdder cuando:

  • El contador se incrementa desde muchos hilos de forma concurrente (por ejemplo: métricas por solicitud en un servidor web).
  • Lo lees raramente (cada pocos segundos para un panel).

Usa AtomicLong cuando:

  • El incremento es poco frecuente o de un solo hilo.
  • Necesitas una lectura precisa e instantánea.

LongAdder es uno de los contadores concurrentes más rápidos que existen — pero el precio es que sum() no es atómico con los incrementos concurrentes. Para el caso típico de informes de métricas, eso está bien.

Lo que las variables atómicas no son

Las variables atómicas escalan a un solo campo. No se componen entre múltiples campos:

AtomicInteger a = new AtomicInteger();
AtomicInteger b = new AtomicInteger();

a.incrementAndGet();                                  // atomic on its own
b.incrementAndGet();                                  // atomic on its own
// but the pair is NOT atomic — another thread can see new a, old b

Si tu invariante abarca múltiples campos ("a == b + 1 siempre"), necesitas un bloqueo (o un único atómico sobre un objeto contenedor con ambos).

Las variables atómicas tampoco ayudan con la visibilidad de campos no relacionados. Escribir en un atómico no publica otros campos como lo hace volatile. Haz que esos otros campos sean volatile (o final, o escríbelos a través del atómico).

compareAndExchange y la nueva API (Java 9+)

Java 9 añadió compareAndExchange (devuelve el valor actual, no solo un boolean):

int prev = counter.compareAndExchange(expected, newVal);
if (prev == expected) {                               // we won
  ...
} else {                                              // somebody else got there first
  // prev is the actual current value
}

Java 9 también añadió la API VarHandle que expone CAS débil, acceso ordenado, etc., para bibliotecas concurrentes de bajo nivel. Raramente la necesitarás; se menciona aquí para que hayas visto el nombre.

Un ejemplo práctico: contador y snapshot

El programa siguiente contrasta cuatro contadores: sin sincronización, volatile, AtomicInteger y LongAdder. Los cuatro son golpeados por 8 hilos que realizan 100.000 incrementos cada uno.

java— editable, runs on the server

Lo que se puede extraer de la ejecución:

  • plain y volatile perdieron actualizaciones — a veces de forma espectacular (un conteo final muy inferior a los 800.000 esperados). volatile corrige el problema de visibilidad, pero n++ sigue siendo tres operaciones. Esto es lo más importante que recordar sobre volatile: no hace que las actualizaciones compuestas sean atómicas.
  • AtomicInteger produjo exactamente el conteo esperado en cada ejecución. El coste por incremento fue de unos pocos nanosegundos — significativamente mayor que n++ sobre un int simple (que es uno o dos), pero sin adquisiciones de bloqueo ni bloqueo de hilos. Bajo contención es más rápido que synchronized por un amplio margen.
  • LongAdder fue el contador más rápido bajo la carga de 8 hilos — dispersa las escrituras entre celdas separadas por CPU, de modo que los hilos no contienden sobre una sola línea de caché. El precio es que sum() no es atómico con increment() (un lector puede ver un total ligeramente desactualizado), lo cual es exactamente el trato correcto para métricas y contadores donde la precisión instantánea no importa.
  • El máximo con bucle CAS registró el valor más grande visto entre todas las muestras. El bucle es el patrón general: leer el valor actual, calcular el nuevo valor deseado, intentar escribirlo; si alguien más escribió primero, el CAS falla y se reintenta. La mayoría de las llamadas a updateAndGet y accumulateAndGet son este bucle con el boilerplate oculto.
  • El AtomicReference<List<String>> produjo un snapshot inmutable del log. Cada escritor construyó una nueva copia inmutable e intentó publicarla; bajo contención, dos escritores pueden construir ambos una copia y el CAS de uno falla — ese hilo reintenta, lee la lista recién actualizada y la fusiona. El patrón es costoso bajo alta contención (muchas copias descartadas) pero ideal para snapshots de "lectura intensiva, reconstruidos ocasionalmente".

Qué sigue

El próximo capítulo, Java Locks, comienza con la historia de java.util.concurrent.locks — la interfaz Lock, por qué existe junto a synchronized, y las capacidades (tryLock, lockInterruptibly, Condition) que añade y que el monitor intrínseco no tiene.

Práctica

Práctica
¿Cuál es la forma correcta de incrementar de manera segura un contador compartido desde muchos hilos en un bucle cerrado?
¿Cuál es la forma correcta de incrementar de manera segura un contador compartido desde muchos hilos en un bucle cerrado?
Was this page helpful?