CodeGym /Cursos /JAVA 25 SELF /ReentrantLock y ReadWriteLock: diferencias y ejemplos

ReentrantLock y ReadWriteLock: diferencias y ejemplos

JAVA 25 SELF
Nivel 52 , Lección 2
Disponible

1. Clase ReentrantLock: bloqueo flexible

La palabra clave synchronized es perfecta para casos básicos: se puede proteger un método o un bloque de código de forma rápida y sencilla. Pero a veces queremos más:

  • Gestionar el bloqueo de forma explícita (por ejemplo, intentar adquirirlo y, si no se consigue, no esperar).
  • Separar permisos de «lectura» y «escritura» sobre el recurso.
  • Interrumpir la espera del bloqueo.
  • Diagnosticar quién y cuándo adquirió o liberó el bloqueo.

Para estas tareas existen las clases ReentrantLock y ReadWriteLock. Ofrecen más control y posibilidades que el viejo y conocido synchronized.

¿Qué es exactamente?

ReentrantLock es una clase que implementa la interfaz Lock. Funciona de forma parecida a synchronized, pero con prestaciones adicionales. La diferencia principal es que la gestión del bloqueo pasa a ser explícita: tú llamas a los métodos lock() y unlock().

Un detalle interesante: la palabra reentrant significa que un hilo puede adquirir el mismo candado varias veces seguidas sin provocar interbloqueo. Es útil si un método se llama a sí mismo de forma recursiva o si trabaja con un bloqueo común en una cadena de llamadas.

Sintaxis de uso

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Counter {
    private int value = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock(); // Adquirimos el bloqueo
        try {
            value++;
        } finally {
            lock.unlock(); // ¡Liberamos el bloqueo obligatoriamente!
        }
    }

    public int getValue() {
        lock.lock();
        try {
            return value;
        } finally {
            lock.unlock();
        }
    }
}

Ten en cuenta:
Las llamadas lock() y unlock() siempre deben ir acompañadas de un bloque try...finally. Si olvidas llamar a unlock(), ningún otro hilo podrá entrar en la sección protegida — obtendrás un «atasco eterno».

Posibilidades de ReentrantLock

Intento de adquisición:
Se puede intentar adquirir el bloqueo, pero sin esperar indefinidamente:

if (lock.tryLock()) {
    try {
        // Trabajamos
    } finally {
        lock.unlock();
    }
} else {
    // No se pudo adquirir — hacemos otra cosa
}

Espera con tiempo de espera:

if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
    // Adquirido en 100 ms
}

Comprobar si el bloqueo está adquirido:

if (lock.isLocked()) { ... }

Diagnóstico de la cola de espera, «equidad» del bloqueo y otras ventajas.

3. Ejemplo: incremento de un contador con ReentrantLock

Vamos a ampliar nuestra aplicación de consola (por ejemplo, simulando el procesamiento de pedidos desde distintos hilos). Comparemos cómo se trabaja con synchronized y con ReentrantLock.

Ejemplo con synchronized

public class OrderCounter {
    private int count = 0;

    public synchronized void increment() {
        count++;
    }

    public synchronized int getCount() {
        return count;
    }
}

Ejemplo equivalente con ReentrantLock

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class OrderCounter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();

    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }

    public int getCount() {
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }
}

¿Cuál es el beneficio?

  • Puedes intentar adquirir el bloqueo sin esperar indefinidamente (tryLock()).
  • Permite implementar lógica compleja: por ejemplo, adquirir varios bloqueos en un orden determinado (relevante para estructuras de datos complejas).
  • Puedes «desbloquear» desde otro lugar (pero hay que hacerlo con mucho cuidado — recuerda siempre llamar a unlock()!).

4. ReadWriteLock: bloqueo para lectura y escritura

¿Qué es?

ReadWriteLock no es solo un candado, sino un distribuidor de acceso inteligente. Su implementación principal es ReentrantReadWriteLock, que divide los bloqueos en dos categorías: lectura y escritura.

Cuando los hilos solo leen datos y nadie modifica nada, pueden trabajar simultáneamente — la lectura no interfiere con la lectura. Pero en cuanto alguien decide realizar un cambio, los demás deben esperar: la escritura admite a un único participante y requiere exclusividad.

Este enfoque es especialmente útil donde hay muchas lecturas y pocas modificaciones — por ejemplo, en un catálogo de productos que los usuarios consultan constantemente, pero que se actualiza solo de vez en cuando.

Sintaxis de uso

import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ProductCatalog {
    private final Map<String, String> products = new HashMap<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void addProduct(String id, String name) {
        rwLock.writeLock().lock();
        try {
            products.put(id, name);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    public String getProduct(String id) {
        rwLock.readLock().lock();
        try {
            return products.get(id);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

Ejemplo de uso en nuestra aplicación

Supongamos que tenemos una base de pedidos que todos los hilos leen (por ejemplo, para buscar un pedido), pero de vez en cuando llegan nuevos pedidos (operación de escritura).

import java.util.*;
import java.util.concurrent.locks.*;

public class OrderDatabase {
    private final List<String> orders = new ArrayList<>();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    // Adición de pedido (requiere writeLock)
    public void addOrder(String order) {
        rwLock.writeLock().lock();
        try {
            orders.add(order);
        } finally {
            rwLock.writeLock().unlock();
        }
    }

    // Obtención de una copia de todos los pedidos (se puede leer en paralelo)
    public List<String> getOrders() {
        rwLock.readLock().lock();
        try {
            // Devolvemos una copia para evitar condiciones de carrera
            return new ArrayList<>(orders);
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

¿Qué ocurre?

  • Mientras nadie escriba, incluso miles de hilos pueden leer la lista de pedidos simultáneamente.
  • En cuanto un hilo empieza a añadir un pedido, las lecturas se bloquean para evitar datos «inconsistentes».

5. Comparativa: ¿cuándo usar cada uno?

Escenario synchronized ReentrantLock ReadWriteLock
Sincronización simple ✖ (excesivo)
Se necesita timeout/intento de adquisición
Muchas lecturas, pocas escrituras ✔ (mejora significativa)
Se necesita diagnóstico/estadísticas
Bloqueo recursivo ✔ (reentrancia)

Conclusión:

  • Para casos simples — usa synchronized.
  • Para mayor flexibilidad — ReentrantLock.
  • Para escenarios de «muchas lecturas y pocas escrituras» — ReadWriteLock.

6. Visualización: esquema de funcionamiento de ReadWriteLock

flowchart LR
    subgraph Lectura
      T1[Hilo 1] -- Lectura --> Orders
      T2[Hilo 2] -- Lectura --> Orders
      T3[Hilo 3] -- Lectura --> Orders
    end
    subgraph Escritura
      T4[Hilo 4] -- Escritura (addOrder) --> Orders
    end
    Orders[Lista de pedidos]
    style Orders fill:#f9f,stroke:#333,stroke-width:2px

Mientras ningún hilo escriba, todos pueden leer a la vez. En cuanto hay una escritura, los demás hilos esperan a que termine writeLock.

7. Particularidades de la implementación y matices

«Equidad» (fairness)

En ReentrantLock y ReentrantReadWriteLock se puede activar el modo «justo» (fair mode): los hilos se atienden en orden de cola, y no «el que antes llega, antes bloquea». Esto evita la «inanición» de hilos, pero puede reducir el rendimiento.

Lock fairLock = new ReentrantLock(true); // true — modo justo
ReadWriteLock fairRWLock = new ReentrantReadWriteLock(true);

Trampas potenciales

  • unlock olvidado: Si no llamas a unlock(), obtendrás un bloqueo permanente. Usa siempre try...finally.
  • Excepciones dentro del bloqueo: Aunque se produzca una excepción dentro de la sección crítica, ¡el bloqueo debe liberarse!
  • Uso excesivo de ReadWriteLock: Para colecciones pequeñas o si casi siempre se escribe, tiene poco sentido usar ReadWriteLock, y el código se complica.

8. Errores típicos

Error n.º 1: olvidaste llamar a unlock()
El error más habitual y traicionero es olvidar llamar a unlock() tras adquirir el bloqueo. Como resultado, bloqueo permanente y los hilos «colgados». Usa siempre try...finally, incluso si parece que «aquí no puede fallar nada».

Error n.º 2: usar ReadWriteLock donde no hace falta
Si apenas hay lecturas concurrentes y la escritura es frecuente, ReadWriteLock solo complicará el código y reducirá el rendimiento. Úsalo solo donde realmente haya muchos lectores simultáneos.

Error n.º 3: adquirir varios bloqueos en distinto orden
Si tu código adquiere varios Lock (por ejemplo, de varios objetos), hazlo siempre en el mismo orden en todos los hilos. De lo contrario puedes provocar un deadlock — los hilos se esperarán mutuamente para siempre.

Error n.º 4: intentar sustituir synchronized por ReentrantLock «porque sí»
No tiene sentido cambiar a la ligera todos los synchronized por Lock — no siempre acelera el programa y puede hacer el código menos legible.

Error n.º 5: olvidar la reentrancia
Si el mismo hilo llama a lock() varias veces seguidas — esto es normal para ReentrantLock, pero no olvides que debes llamar a unlock() el mismo número de veces.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION