2 Variables atómicas¶
Variables atómicas¶
Java proporciona clases wrappers atómicas para los tipos más habituales, como AtomicInteger
, AtomicLong
y AtomicBoolean
, junto con una versión para la referencia a cualquier variable, AtomicReference
.
Estas clases proporcionan métodos específicos para realizar operaciones básicas sobre las variables de manera atómica, como:
get()
: Obtiene el valor contenido en la variable, asegurando la visibilidad (volatile)set(newValue)
: Establece el valor de la variable, asegurando la visibilidad (volatile).getAndSet(newValue)
: Establece el valor de la variable, y retorna el valor antiguo, todo ello de manera atómica.compareAndSet(expectedValue, newValue)
: Actualiza el valor de la variable anewValue
sólo si el valor actual corresponde aexpectedValue
, todo ello de manera atómica. Retornatrue
si se ha realizado la actualización.
Para las clases atómicas correspondientes a tipos numéricos tendremos también disponibles además operaciones atómicas de suma/resta, incremento y decremento:
addAndGet(cantidad)
: Suma la cantidad indicada al valor de la variable, lo establece como nuevo valor de la misma y lo retorna, todo ello de forma atómica.getAndAdd(cantidad)
: Suma la cantidad indicada al valor de la variable, lo establece como nuevo valor de la misma, pero retorna el valor que tenía la variable antes de la actualización, todo ello de forma atómica.decrementAndGet()
ygetAndDecrement()
:incrementAndGet()
ygetAndIncrement()
:
Además, también proporciona clases wrappers atómicas para los tipos de arrays más habituales, como AtomicIntegerArray
, AtomicLongArray
y AtomicReferenceArray
.
Una de las operaciones típicas del tipo check then act (comprobar y luego actuar) es cuando se quiere realizar una tarea una única vez y para ello se utiliza variable booleana que actúa como bandera (flag). Si el objeto que contiene la bandera es accedido para escritura desde varios hilos es posible que se produzcan condiciones de carrera (race conditions) según los cuales la tarea sea ejecutada más de una vez, porque se solapen la comprobación del valor de la bandera desde dichos hilos antes de que cambien el valor de la misma. Para solucionar este problema podemos hacer que la bander sea una variable AtomicBoolean
. Veamos un ejemplo:
public class Event<T> {
private T content;
private AtomicBoolean handled = new AtomicBoolean(false);
public Event(T content) {
this.content = content;
}
// Retorna el contenido si aún no ha sido consumido, o null en caso contrario.
public T getContentIfNotHandled() {
if (handled.compareAndSet(false, true)) {
return content;
} else {
return null;
}
}
}
Como vemos, la librería de concurrencia de Java sólo proporciona clases atomic para los tipos más frecuentes, pero carece de las clases AtomicFloat
o AtomicDouble
.
Para esos casos podemos usar la clase AtomicInteger
y a la hora de retornar o recibir los datos usar los métodos Float.floatToIntBits
y Float.intBitsToFloat
o Double.doubleToLongBits
y Double.longBitsToDouble
para realizar las conversiones adecuadas (puede ver las clases correspondientes en el proyecto AtomicLong, aunque no se usan en él, obtenidas de este enlace). Puede utilizar también las clasesDoubleAccumulator
o DoubleAdder
que veremos en el siguiente apartado.
Proyecto AtomicLong¶
En este proyecto simularemos el funcionamiento de una cuenta bancaria, en la que una empresa realiza abonos desde un hilo y el banco realiza cargos desde otro hilo. El saldo de la cuenta será almacenado usando una variable atómica.
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
public class Main {
public static void main(String[] args) throws InterruptedException {
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss");
Account account = new Account(0);
// Show initial balance.
System.out.printf("%s -> Initial balance: %d€\n",
LocalTime.now().format(dateTimeFormatter), account.getBalance());
// Start both saver and consumer threads. Both threads share the same account.
Thread saverThread = new Thread(new AccountSaver(account));
saverThread.start();
Thread consumerThread = new Thread(new AccountConsumer(account));
consumerThread.start();
// Wait for both threads to finish.
saverThread.join();
consumerThread.join();
// Show final balance.
System.out.printf("\n%s -> Final balance: %d€\n",
LocalTime.now().format(dateTimeFormatter), account.getBalance());
}
}
import java.util.concurrent.atomic.AtomicLong;
public class Account {
private final AtomicLong balance;
public Account(long initialBalance) {
balance = new AtomicLong(initialBalance);
}
public long getBalance() {
return balance.get();
}
public void deposit(long amount) {
balance.addAndGet(amount);
}
public void debit(long amount) {
balance.addAndGet(-amount);
}
}