Un reto que me propuse cuando estaba empezando con Java y que quiero plantearte a ti, es tener una buena estrategia de sincronización. ¿Por qué te lo digo? porque si llegas a desarrollar una aplicación donde varios hilos comparten datos, vas a necesitarla. Personalmente me ha servido bastante java.util.concurrent.ReentrantLock, ya que me ha permitido tener un control total sobre la sincronización y de paso eliminar los problemas de concurrencia. Por experiencia te digo que en algún momento te encontrarás con conflictos en la sincronización de tus hilos, así que mejor aprender a usar ReentrantLock lo antes posible.
¿Qué es java.util.concurrent.ReentrantLock?
Este concepto es de los más fáciles de entender. Resulta que java.util.concurrent.ReentrantLock es una clase de la biblioteca concurrente de Java diseñada para manejar la sincronización entre múltiples hilos con mayor flexibilidad y control que la palabra clave synchronized.
A diferencia de los bloques sincronizados, los locks tienen estas características:
- Interrupción de hilos en espera.
- Establecimiento de tiempos de espera.
- Verificación de hilos en cola.
Además, como tiene la funcionalidad de “reentrada” permite que un hilo que ya posea el lock pueda volver a adquirirlo sin quedar bloqueado.
Métodos clave de java.util.concurrent.ReentrantLock
Método | Descripción |
---|---|
lock() | Adquiere el lock. Si no está disponible, el hilo se bloquea hasta obtenerlo. |
unlock() | Libera el lock y permite que otros hilos lo adquieran. |
tryLock() | Intenta adquirir el lock sin bloquear el hilo. Devuelve true si tiene éxito. |
lockInterruptibly() | Permite interrumpir un hilo mientras espera el lock. |
getHoldCount() | Devuelve cuántas veces el lock ha sido adquirido por el hilo actual. |
Ventajas de usar java.util.concurrent.ReentrantLock
- Mayor control: Algo grandioso es que los métodos como tryLock() y lockInterruptibly() te dejan manejar escenarios complejos.
- Evita bloqueos: Puedes establecer tiempos de espera para adquirir el lock.
- Reutilizable: Su funcionalidad de reentrada es útil en escenarios donde el mismo hilo necesita acceder varias veces al recurso bloqueado.
Cómo funciona java.util.concurrent.ReentrantLock: Ejemplos prácticos
Ten en cuenta estos 3 aspectos que te dejo aquí, para que entiendas el funcionamiento de java.util.concurrent.ReentrantLock:
- Adquirir el lock: Llama a lock() o tryLock() para garantizar acceso exclusivo al recurso.
- Realizar la operación crítica: Ejecuta el código que necesita protección.
- Liberar el lock: Llama a unlock() para permitir que otros hilos accedan al recurso.
Control de acceso a un contador compartido
Mira que cada hilo incrementa el contador compartido de forma segura gracias a ReentrantLock. Además, el lock garantiza que solo un hilo pueda acceder a la sección crítica a la vez.
import java.util.concurrent.locks.ReentrantLock;
public class SharedCounter {
private static int counter = 0;
private static final ReentrantLock lock = new ReentrantLock();
public static void incrementCounter() {
lock.lock();
try {
counter++;
System.out.println(Thread.currentThread().getName() + " incrementó el contador a: " + counter);
} finally {
lock.unlock();
}
}
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 3; i++) {
incrementCounter();
}
};
Thread t1 = new Thread(task, "Hilo-1");
Thread t2 = new Thread(task, "Hilo-2");
t1.start();
t2.start();
}
}
Gestión de accesos a un sistema de reservas
Observa que el método tryLock() permite verificar si el lock está disponible, evitando bloqueos innecesarios. Si un cliente no puede reservar, se imprime un mensaje en lugar de esperar indefinidamente.
import java.util.concurrent.locks.ReentrantLock;
public class BookingSystem {
private static final ReentrantLock lock = new ReentrantLock();
public static void bookSeat(String seat) {
if (lock.tryLock()) {
try {
System.out.println(Thread.currentThread().getName() + " reservó el asiento: " + seat);
Thread.sleep(1000); // Simula la reserva
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
System.out.println(Thread.currentThread().getName() + " no pudo reservar el asiento: " + seat);
}
}
public static void main(String[] args) {
Runnable task = () -> bookSeat("A1");
Thread t1 = new Thread(task, "Cliente-1");
Thread t2 = new Thread(task, "Cliente-2");
t1.start();
t2.start();
}
}
Alternar tareas entre hilos
Aquí usé Condition para alternar la ejecución entre dos hilos. También puedes ver que signal() y await() son los que dejan coordinar el turno de ejecución.
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class AlternatingTasks {
private static final ReentrantLock lock = new ReentrantLock();
private static final Condition condition = lock.newCondition();
private static boolean turn = true; // true para tarea A, false para tarea B
public static void main(String[] args) {
Thread taskA = new Thread(() -> {
for (int i = 0; i < 5; i++) {
lock.lock();
try {
while (!turn) {
condition.await();
}
System.out.println("Tarea A ejecutando");
turn = false;
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
Thread taskB = new Thread(() -> {
for (int i = 0; i < 5; i++) {
lock.lock();
try {
while (turn) {
condition.await();
}
System.out.println("Tarea B ejecutando");
turn = true;
condition.signal();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
});
taskA.start();
taskB.start();
}
}
Creo que ya te diste cuenta de que java.util.concurrent.ReentrantLock es excelente para manejar la sincronización en Java con mayor flexibilidad que los bloques sincronizados tradicionales. Úsalo para que no tengas conflictos en entornos multihilo y crees aplicaciones más complejas.
¿Quieres dominar herramientas como esta y convertirte en un experto en Java? Apúntate al Bootcamp de Java Full Stack de KeepCoding. Aprende desde los conceptos básicos hasta técnicas avanzadas que transformarán tu carrera. ¡El momento de destacar en el sector IT es ahora!