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:
- Leer el valor de la variable (por ejemplo, 5).
- Incrementar ese valor en 1.
- 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 |
|---|---|
|
Obtener el valor actual |
|
Establecer el valor |
|
Incrementar en 1 y devolver el nuevo valor |
|
Devolver el valor actual y luego incrementarlo en 1 |
|
Incrementar en delta y devolver el nuevo valor |
|
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).
GO TO FULL VERSION