7 - Locks explícitos (java.util.concurrent.locks)

Los locks explícitos permiten más control que synchronized: timeouts, comprobación no bloqueante y varias condiciones por lock. Exigen disciplina para evitar fugas y deadlocks.

7.1 ReentrantLock

  • Ventajas: operación no bloqueante (tryLock()), desbloqueo en orden justo opcional y múltiples condiciones.
  • Control manual: siempre emparejar lock() con unlock() en un bloque finally.
  • Timeout: tryLock(timeout, unit) permite abortar si no se adquiere el lock.
  • Condiciones: newCondition() crea colas de espera independientes, separando señales.
  • Justicia: new ReentrantLock(true) intenta servir en orden FIFO; reduce throughput (cantidad de trabajo completado por unidad de tiempo).
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class BufferLock {
  private final ReentrantLock lock = new ReentrantLock(true); // justo
  private final Condition noVacio = lock.newCondition();
  private final Condition noLleno = lock.newCondition();
  private int valor;
  private boolean disponible = false;

  public void producir(int nuevo) throws InterruptedException {
    lock.lock();
    try {
      while (disponible) {
        noLleno.await();
      }
      valor = nuevo;
      disponible = true;
      noVacio.signal();
    } finally {
      lock.unlock();
    }
  }

  public int consumir() throws InterruptedException {
    lock.lock();
    try {
      while (!disponible) {
        if (!noVacio.await(1, TimeUnit.SECONDS)) {
          throw new IllegalStateException("Timeout esperando datos");
        }
      }
      disponible = false;
      noLleno.signal();
      return valor;
    } finally {
      lock.unlock();
    }
  }
 
  public static void main(String[] args) throws Exception {
    BufferLock buffer = new BufferLock();

    Runnable productor = () -> {
      for (int i = 1; i <= 5; i++) {
        try {
          buffer.producir(i);
          System.out.println("Producido " + i);
          TimeUnit.MILLISECONDS.sleep(150);
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          return;
        }
      }
    };

    Runnable consumidor = () -> {
      for (int i = 1; i <= 5; i++) {
        try {
          int dato = buffer.consumir();
          System.out.println("Consumido " + dato);
          TimeUnit.MILLISECONDS.sleep(250);
        } catch (InterruptedException e) {
          Thread.currentThread().interrupt();
          return;
        }
      }
    };

    Thread tProd = new Thread(productor, "prod");
    Thread tCons = new Thread(consumidor, "cons");
    tProd.start();
    tCons.start();
    tProd.join();
    tCons.join();
    System.out.println("Terminado");
  }
}

Partes clave:

  • Lock justo: new ReentrantLock(true) reduce hambre repartiendo el acceso en orden FIFO.
  • Condiciones separadas: noVacio y noLleno representan colas independientes para esperar datos o espacio.
  • Esperas en bucle: los while protegen de señales espurias y revalidan la condición antes de continuar.
  • Timeout en consumidor: await(1, TimeUnit.SECONDS) evita bloqueos indefinidos y lanza excepción si no hay progreso.
  • Señales puntuales: el productor hace noVacio.signal() y el consumidor noLleno.signal() para despertar solo al grupo relevante.
  • Estructura de prueba: productor y consumidor recorren 5 iteraciones con pausas distintas para observar el intercalado y verificar la coordinación.
  • Trade-off de justicia: habilitar fairness reduce el riesgo de hambre pero baja el throughput porque limita reordenamientos que podrían ser más rápidos.

7.2 ReadWriteLock

  • readLock() concurrente: permite múltiples lectores simultáneos.
  • writeLock() exclusivo: un solo escritor, bloquea lectores y otros escritores.
  • Uso ideal: estructuras con muchas lecturas y pocas escrituras (cachés, catálogos, configuración compartida).

7.3 StampedLock

  • Lectura optimista: tryOptimisticRead() permite leer sin bloquear y validar si hubo escrituras.
  • Lectura compartida: readLock() para lecturas coordinadas.
  • Escritura exclusiva: writeLock() para mutaciones.
  • Ejemplo típico: cálculos geométricos o estructuras 3D donde la mayoría son lecturas rápidas.

7.4 Problemas comunes con locks

  • No liberar locks: olvidar unlock() lleva a deadlocks.
  • Combinar locks distintos: adquirir en orden inconsistente genera interbloqueos.
  • Bloqueo excesivo: sección crítica demasiado grande aumenta latencia.
  • Fairness: la justicia evita hambre pero reduce rendimiento; evaluar el trade-off.