La programación multihilo es una de las herramientas más potentes de Java, utilizada para realizar múltiples tareas simultáneamente. Este artículo presenta una visión intermedia de los hilos, explorando cómo funcionan y cómo se pueden aplicar a escenarios más prácticos, abordando problemas comunes como la sincronización y la comunicación entre hilos.
¿Qué es un Hilo?
Un hilo en Java es una unidad de ejecución dentro de un programa. Un programa puede tener múltiples hilos trabajando de manera concurrente, lo que permite realizar varias tareas al mismo tiempo dentro del mismo proceso.
Cada hilo tiene su propio ciclo de vida, que incluye los siguientes estados:
- Nuevo (New): El hilo se crea, pero aún no ha comenzado.
- Ejecutable (Runnable): Está listo para ejecutarse, pero esperando a que el planificador del sistema operativo lo asigne a la CPU.
- Ejecutando (Running): El hilo está en ejecución.
- En Espera/Bloqueado (Waiting/Blocked): Está esperando por un recurso o una señal.
- Terminado (Terminated): El hilo ha completado su tarea.
Creación y Gestión de Hilos
1. Extender la Clase Thread
class HiloSimple extends Thread {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Hilo: " + Thread.currentThread().getName() + " - Iteración: " + i);
try {
Thread.sleep(1000); // Pausa de 1 segundo
} catch (InterruptedException e) {
System.out.println("Hilo interrumpido.");
}
}
}
}
public class Main {
public static void main(String[] args) {
HiloSimple hilo1 = new HiloSimple();
hilo1.start(); // Inicia el hilo
}
}
2. Implementar la Interfaz Runnable
Es más flexible, ya que permite heredar de otras clases.
class TareaSimple implements Runnable {
@Override
public void run() {
for (int i = 1; i <= 5; i++) {
System.out.println("Tarea en ejecución: Iteración " + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("Tarea interrumpida.");
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread hilo1 = new Thread(new TareaSimple());
hilo1.start();
}
}
Sincronización de Hilos
Cuando varios hilos acceden a un recurso compartido, puede haber conflictos conocidos como condiciones de carrera. Para evitar esto, Java proporciona mecanismos de sincronización como synchronized
.
Ejemplo: Contador Sincronizado
class Contador {
private int contador = 0;
public synchronized void incrementar() {
contador++;
System.out.println(Thread.currentThread().getName() + " - Contador: " + contador);
}
}
public class Main {
public static void main(String[] args) {
Contador contador = new Contador();
Runnable tarea = contador::incrementar;
Thread hilo1 = new Thread(tarea, "Hilo-1");
Thread hilo2 = new Thread(tarea, "Hilo-2");
hilo1.start();
hilo2.start();
}
}
Salida esperada:
Hilo-1 - Contador: 1
Hilo-2 - Contador: 2
Bloques Síncronos
En lugar de sincronizar métodos enteros, puedes sincronizar solo una sección crítica:
public void incrementar() {
synchronized (this) {
contador++;
System.out.println(Thread.currentThread().getName() + " - Contador: " + contador);
}
}
Comunicación entre Hilos
Los hilos a menudo necesitan coordinarse. Esto se logra con métodos como wait()
, notify()
y notifyAll()
, que deben ser llamados dentro de un bloque sincronizado.
Ejemplo: Productor-Consumidor
class Buffer {
private int dato;
private boolean disponible = false;
public synchronized void producir(int valor) throws InterruptedException {
while (disponible) {
wait();
}
dato = valor;
disponible = true;
System.out.println("Producido: " + dato);
notify();
}
public synchronized int consumir() throws InterruptedException {
while (!disponible) {
wait();
}
disponible = false;
System.out.println("Consumido: " + dato);
notify();
return dato;
}
}
public class Main {
public static void main(String[] args) {
Buffer buffer = new Buffer();
Thread productor = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
buffer.producir(i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumidor = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
buffer.consumir();
Thread.sleep(1500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
productor.start();
consumidor.start();
}
}
Salida esperada:
Producido: 1
Consumido: 1
Producido: 2
Consumido: 2
...
Ejecutores (Executors
)
Para gestionar un gran número de hilos, Java proporciona el framework de ejecutores, que simplifica la creación y el manejo de hilos.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService ejecutor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 10; i++) {
final int tarea = i;
ejecutor.execute(() -> {
System.out.println("Ejecutando tarea " + tarea + " en " + Thread.currentThread().getName());
});
}
ejecutor.shutdown();
}
}
Conclusión
Los hilos en Java son herramientas poderosas que permiten manejar múltiples tareas concurrentemente. Con una comprensión intermedia, puedes aprovechar mecanismos como la sincronización, comunicación entre hilos y ejecutores para construir aplicaciones robustas y eficientes. Sin embargo, es importante manejar cuidadosamente los recursos compartidos para evitar problemas como condiciones de carrera o interbloqueos. Con práctica y diseño adecuado, puedes dominar la programación multihilo en Java.