La programación multihilo es una de las habilidades más desafiantes y útiles en el desarrollo de aplicaciones modernas. Este artículo explora los conceptos avanzados de hilos en Java y presenta un ejercicio práctico para poner a prueba tus conocimientos en programación concurrente.
Conceptos Clave para Trabajar con Hilos en Java
Sincronización de Recursos Compartidos
Cuando varios hilos acceden a un recurso compartido, como una lista o una variable, es crucial usar mecanismos de sincronización para evitar condiciones de carrera. Java ofrece herramientas como synchronized
, wait()
, notify()
, y clases avanzadas como ReentrantLock
.
Comunicación entre Hilos
La comunicación entre hilos permite que un hilo espere a que otro complete su tarea. Esto se logra utilizando los métodos wait()
, notify()
, y notifyAll()
en combinación con bloques sincronizados.
Gestión Avanzada de Hilos
Además de las herramientas básicas, Java proporciona el framework ExecutorService
para gestionar grandes cantidades de hilos de manera eficiente, junto con herramientas como Callable
, Future
, y CompletableFuture
para manejar tareas con resultados.
Ejercicio: Simulación de una Fábrica Multihilo con Productores y Consumidores
Enunciado
Diseña un programa en Java que simule el funcionamiento de una fábrica con múltiples estaciones de producción y ensamblaje. En esta fábrica:
- Estaciones de producción: Generan piezas que se almacenan en un buffer compartido (almacén).
- Estaciones de ensamblaje: Toman piezas del buffer para construir un producto.
El sistema debe garantizar que:
- Las estaciones de ensamblaje no intenten tomar piezas si el buffer está vacío.
- Las estaciones de producción no sobrecarguen el buffer si está lleno.
- Los accesos al buffer estén sincronizados para evitar condiciones de carrera.
Requisitos Técnicos
- Clases necesarias:
- Una clase
Buffer
que represente el almacén compartido. - Una clase
Productor
que genere piezas y las almacene en el buffer. - Una clase
Consumidor
que retire piezas del buffer para ensamblar productos.
- Una clase
- Sincronización:
- Usa
wait()
ynotifyAll()
para coordinar los accesos al buffer.
- Usa
- Simulación:
- Crea 3 productores y 2 consumidores.
- Cada productor genera 10 piezas.
- Cada consumidor ensambla productos hasta que no queden piezas en el buffer.
Implementación
import java.util.LinkedList;
import java.util.Queue;
class Buffer {
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacidad;
public Buffer(int capacidad) {
this.capacidad = capacidad;
}
public synchronized void producir(int pieza) throws InterruptedException {
while (buffer.size() == capacidad) {
wait(); // Espera si el buffer está lleno
}
buffer.add(pieza);
System.out.println(Thread.currentThread().getName() + " produjo: " + pieza);
notifyAll(); // Notifica a los consumidores
}
public synchronized int consumir() throws InterruptedException {
while (buffer.isEmpty()) {
wait(); // Espera si el buffer está vacío
}
int pieza = buffer.poll();
System.out.println(Thread.currentThread().getName() + " consumió: " + pieza);
notifyAll(); // Notifica a los productores
return pieza;
}
}
class Productor implements Runnable {
private final Buffer buffer;
public Productor(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 1; i <= 10; i++) {
buffer.producir(i);
Thread.sleep(500); // Simula tiempo de producción
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumidor implements Runnable {
private final Buffer buffer;
public Consumidor(Buffer buffer) {
this.buffer = buffer;
}
@Override
public void run() {
try {
for (int i = 1; i <= 15; i++) {
buffer.consumir();
Thread.sleep(1000); // Simula tiempo de ensamblaje
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class Fabrica {
public static void main(String[] args) {
Buffer buffer = new Buffer(5); // Buffer con capacidad máxima de 5 piezas
// Crear y ejecutar 3 productores
for (int i = 1; i <= 3; i++) {
new Thread(new Productor(buffer), "Productor-" + i).start();
}
// Crear y ejecutar 2 consumidores
for (int i = 1; i <= 2; i++) {
new Thread(new Consumidor(buffer), "Consumidor-" + i).start();
}
}
}
Requisitos de Salida
El programa debe mostrar mensajes similares a estos (el orden puede variar debido a la concurrencia):
Productor-1 produjo: 1
Productor-2 produjo: 2
Productor-3 produjo: 3
Consumidor-1 consumió: 1
Productor-1 produjo: 4
Consumidor-2 consumió: 2
Consumidor-1 consumió: 3
Productor-2 produjo: 5
...
Conceptos Evaluados
- Sincronización: Uso correcto de
synchronized
,wait()
ynotifyAll()
para evitar conflictos. - Manejo de recursos compartidos: El buffer debe funcionar correctamente bajo concurrencia.
- Gestión eficiente de hilos: Creación y ejecución de múltiples productores y consumidores.
Resumen
Este ejercicio combina conceptos fundamentales y avanzados de programación multihilo, como sincronización, gestión de recursos compartidos y coordinación entre hilos. Practicar este tipo de problemas te ayudará a dominar las habilidades necesarias para implementar sistemas concurrentes robustos y eficientes.