En Java, la clase ReentrantLock del paquete java.util.concurrent.locks ofrece un bloqueo explícito para gestionar la concurrencia en entornos multihilo. A diferencia de los bloques synchronized, permite un control más flexible, como la capacidad de interrumpir la espera o intentar adquirir el bloqueo sin bloqueo indefinido.
Para ilustrar su uso, consideremos un escenario donde dos hilos intentan retirar dinero de una cuenta bancaria con saldo inicial de 100 unidades. Sin sincronización, ambos hilos podrían leer el saldo simultáneamente y realizar retiros, resultando en un saldo negativo. Implementemos una clase CuentaBancaria que use ReentrantLock para evitar esto.
package ejemplos.concurrencia;
import java.util.concurrent.locks.ReentrantLock;
public class CuentaBancaria implements Runnable {
private int saldo = 100;
private final ReentrantLock cerrojo = new ReentrantLock();
public void retirarFondos() {
cerrojo.lock();
try {
if (saldo >= 100) {
int saldoActual = saldo;
// Simulación de operación costosa
for (int i = 0; i < 10000; i++) { }
saldo -= 100;
System.out.println("Saldo anterior: " + saldoActual +
", hilo: " + Thread.currentThread().getName() +
" retiró 100, saldo restante: " + saldo);
}
} finally {
cerrojo.unlock();
}
}
@Override
public void run() {
retirarFondos();
}
}
Para probar la sincronización, creamos dos hilos que comparten la misma instancia de CuentaBancaria.
package ejemplos.concurrencia;
public class PruebaRetiro {
public static void main(String[] args) {
CuentaBancaria cuenta = new CuentaBancaria();
Thread hiloEsposa = new Thread(cuenta, "Esposa");
Thread hiloEsposo = new Thread(cuenta, "Esposo");
hiloEsposa.start();
hiloEsposo.start();
}
}
Al ejecutar, la salida mostrará que solo un hilo retira exitosamente, evitando el sobregiro. Esto se debe a que el bloqueo ReentrantLock garantiza la exclusión mutua.
Implementación interna de ReentrantLock
ReentrantLock se basa en la infraestructura de AbstractQueuedSynchronizer (AQS). Al invocar lock(), se delega a una subclase Sync, típicamente NonfairSync. El método lock() de NonfairSync intenta adquirir el bloqueo mediante compareAndSetState, una operación CAS (Compare-and-Swap).
El estado del bloqueo se mantiene en una variable int state dentro de AQS, modificada con volatile para visibilidad entre hilos. Un valor state = 0 indica que el bloqueo está libre, mientras que state > 0 significa que está ocupado, con el contador de reentrancia.
El método central nonfairTryAcquire verifica el estado actual:
boolean adquirirNoEquitativo(int adquisiciones) {
Thread hiloActual = Thread.currentThread();
int estadoActual = getState();
if (estadoActual == 0) {
if (compareAndSetState(0, adquisiciones)) {
setExclusiveOwnerThread(hiloActual);
return true;
}
} else if (hiloActual == getExclusiveOwnerThread()) {
int nuevoEstado = estadoActual + adquisiciones;
if (nuevoEstado < 0) throw new Error("Contador excedido");
setState(nuevoEstado);
return true;
}
return false;
}
Si la adquisición falla, el hilo se encola en una lista de espera implementada con nodos Node en AQS. Esta lista es una cola doblemente enlazada, donde la inserción y extracción se realizan mediante operaciones CAS para evitar bloqueos adicionales. Por ejemplo, múltiples hilos que no pueden adquirir el bloqueo se añaden a la cola de forma atómica, usando compareAndSetTail.
El método acquireQueued gestiona la espera, colocando el hilo en un bucle que verifica si debe intentar adquirir el bloqueo nuevamente, optimizando el consumo de CPU. Esta arquitectura permite un rendimiento eficiente en escenarios de alta concurrencia.