Introducción
En la programación concurrente, la sincronización es fundamental para evitar condiciones de carera. Sin embargo, en muchos escenarios reales se requiere colaboración entre hilos. Un ejemplo clásico es el modelo productor-consumidor: cuando la cola está llena, el productor debe esperar a que haya espacio disponible, liberando el acceso al recurso compartido. Mientras tanto, el consumidor debe esperar a que haya elementos disponibles en la cola.
Este artículo explora dos mecanismos principales para lograr la colaboración entre hilos en Java: el uso de Object.wait(), Object.notify() y Object.notifyAll(), y la interfaz Condition.
Métodos wait(), notify() y notifyAll()
Estos tres métodos pertenecen a la clase Object y permiten la comunicación entre hilos que comparten un mismo monitor:
public final native void notify();
public final native void notifyAll();
public final native void wait(long timeout) throws InterruptedException;
Puntos importantes:
- wait(): El hilo actual se bloquea y libera el monitor del objeto. Debe estar dentro de un bloque
synchronized. - notify(): Despierta un único hilo que está esperando el monitor del objeto. La selección del hilo depends del sistema.
- notifyAll(): Despierta todos los hilos que están esperando el monitor del objeto.
¿Por qué están en Object y no en Thread? Porque cada objeto tiene su propio monitor (bloqueo), y tiene sentido que la operación de esperar por un recurso sea realizada sobre ese recurso específico.
Ejemplo demostrativo:
public class DemostracionSincronizacion {
private static final Object recurso = new Object();
public static void main(String[] args) {
HiloEspera hiloEspera = new HiloEspera();
HiloNotificador hiloNotificador = new HiloNotificador();
hiloEspera.start();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
hiloNotificador.start();
}
static class HiloEspera extends Thread {
@Override
public void run() {
synchronized (recurso) {
try {
recurso.wait();
} catch (InterruptedException e) {
// Manejo de interrupción
}
System.out.println("Hilo " + getName() + " adquirió el bloqueo");
}
}
}
static class HiloNotificador extends Thread {
@Override
public void run() {
synchronized (recurso) {
recurso.notify();
System.out.println("Hilo " + getName() + " invocó notify()");
}
System.out.println("Hilo " + getName() + " liberó el bloqueo");
}
}
}
La salida siempre será:
Hilo Thread-1 invocó notify()
Hilo Thread-1 liberó el bloqueo
Hilo Thread-0 adquirió el bloqueo
Esto demuestra que el hilo despertado no adquiere el monitor inmediatametne, sino que debe esperar a que el hilo que llamó notify() libere el bloqueo.
La interfaz Condition
Introducida en Java 1.5, Condition ofrece una alternativa más flexible y segura que los métodos tradicionales de Object. Se utiliza con objetos Lock en lugar de bloques synchronized.
Métodos principales:
await(): Equivalente await()signal(): Equivalente anotify()signalAll(): Equivalente anotifyAll()
La principle ventaja de Condition es que permite tener múltiples condiciones de espera para el mismo bloqueo, algo que no es posible con los métodos de Object.
Implementación del modelo Productor-Consumidor
Utilizando wait() y notify():
public class ModeloProductorConsumidor {
private static final int CAPACIDAD = 10;
private final List<Integer> almacen = new ArrayList<>(CAPACIDAD);
public static void main(String[] args) {
ModeloProductorConsumidor instancia = new ModeloProductorConsumidor();
Productor prod = instancia.new Productor();
Consumidor cons = instancia.new Consumidor();
prod.start();
cons.start();
}
class Consumidor extends Thread {
@Override
public void run() {
consumir();
}
private void consumir() {
while (true) {
synchronized (almacen) {
while (almacen.isEmpty()) {
try {
System.out.println("Almacén vacío, esperando...");
almacen.wait();
} catch (InterruptedException e) {
e.printStackTrace();
almacen.notify();
}
}
almacen.remove(0);
almacen.notify();
System.out.println("Consumido: quedan " + almacen.size() + " elementos");
}
}
}
}
class Productor extends Thread {
@Override
public void run() {
producir();
}
private void producir() {
while (true) {
synchronized (almacen) {
while (almacen.size() == CAPACIDAD) {
try {
System.out.println("Almacén lleno, esperando...");
almacen.wait();
} catch (InterruptedException e) {
e.printStackTrace();
almacen.notify();
}
}
almacen.add(1);
almacen.notify();
System.out.println("Producido: quedan " + (CAPACIDAD - almacen.size()) + " espacios");
}
}
}
}
}
Utilizando Condition:
public class ModeloProductorConsumidorCondition {
private static final int CAPACIDAD = 10;
private final List<Integer> almacen = new ArrayList<>();
private final ReentrantLock mutex = new ReentrantLock();
private final Condition condicionNoLlena = mutex.newCondition();
private final Condition condicionNoVacia = mutex.newCondition();
public static void main(String[] args) {
ModeloProductorConsumidorCondition instancia = new ModeloProductorConsumidorCondition();
Productor prod = instancia.new Productor();
Consumidor cons = instancia.new Consumidor();
prod.start();
cons.start();
}
class Consumidor extends Thread {
@Override
public void run() {
consumir();
}
private void consumir() {
while (true) {
mutex.lock();
try {
while (almacen.isEmpty()) {
System.out.println("Almacén vacío, esperando...");
condicionNoVacia.await();
}
almacen.remove(0);
condicionNoLlena.signal();
System.out.println("Consumido: quedan " + almacen.size() + " elementos");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mutex.unlock();
}
}
}
}
class Productor extends Thread {
@Override
public void run() {
producir();
}
private void producir() {
while (true) {
mutex.lock();
try {
while (almacen.size() == CAPACIDAD) {
System.out.println("Almacén lleno, esperando...");
condicionNoLlena.await();
}
almacen.add(1);
condicionNoVacia.signal();
System.out.println("Producido: quedan " + (CAPACIDAD - almacen.size()) + " espacios");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
mutex.unlock();
}
}
}
}
}
La versión con Condition ofrece mayor control y claridad, especialmente cuando se necesitan múltiples condiciones de espera en un mismo bloqueo.