En programación concurrente, los bloqueos justos e injustos son conceptos esenciales para gestionar el acceso a recursos compartidos. Un bloqueo justo asegura que los hilos adquieran el bloqueo en el orden de llegada (FIFO), similar a una cola ordenada. Un bloqueo injusto permite que los hilos compitan por el bloqueo sin respetar el orden, lo que puede llevar a situaciones donde hilos posteriores obtienen el acceso antes.
Un ejemplo práctico es una cafetería donde los estudiantes esperan en fila para recibir comida. Cada estudiante actúa como un hilo, y el acceso a la comida está controlado por un mecanismo de bloqueo. En un bloqueo justo, los estudiantes son atandidos según su llegada; en un bloqueo injusto, algunos pueden saltarse la cola dependiendo del momento.
En Java, synchronized implementa un bloqueo injusto, como se demuestra en el siguiente ejemplo. Se define una clase Cafeteria con un método servirComida que utiliza synchronized para controlar el acceso. Los hilos representan estudiantes con identificadores únicos.
public class SynchronizedLockDemo {
private static class Cafeteria {
public void servirComida() {
System.out.println(Thread.currentThread().getName() + ": esperando en la cola");
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ": recibiendo la comida");
}
}
}
public static void main(String[] args) {
Cafeteria cafeteria = new Cafeteria();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
cafeteria.servirComida();
}, "Estudiante-" + (i + 1)).start();
}
}
}
Al ejecutar este código, el orden de ejecución puede variar, demostrando que synchronized no garantiza la justicia. Por ejemplo, un hilo que llegó después podría ejecutarse antes si adquiere el bloqueo durante una ventana de oportunidad.
La clase ReentrantLock del paquete java.util.concurrent.locks ofrece tanto bloqueos justos como injustos. Para un bloqueo injusto, se instancia ReentrantLock con el parámetro false. El bloqueo y desbloqueo se gestionan mediante lock() y unlock() dentro de bloques try-finally para asegurar la liberación del recurso.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ReentrantLockUnfairDemo {
private static final Lock lock = new ReentrantLock(false);
private static class Cafe {
public void obtenerComida() {
try {
System.out.println(Thread.currentThread().getName() + ": en cola");
lock.lock();
System.out.println(Thread.currentThread().getName() + ": comida servida");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) throws InterruptedException {
Cafe cafe = new Cafe();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
cafe.obtenerComida();
}, "Cliente-" + (i + 1)).start();
}
}
}
Este código muestra un comportamiento injusto similar a synchronized. Para implementar un bloqueo justo, se cambia el parámetro a true en la instancia de ReentrantLock, lo que garantiza que los hilos adquieran el bloqueo en orden de llegada.
ReentrantLock está construido sobre AbstractQueuedSynchronizer (AQS), que utiliza una cola de espera doblemente enlazada para administrar los hilos. AQS gestiona el estado del bloqueo y las colas de condicinoes para la comunicación entre hilos, ofreciendo mayor flexibilidad que synchronized. En cambio, synchronized es un mecanismo nativo basado en C++ con una única cola de condiciones.