Los hilos (o threads) son fundamentales en la programación concurrente. Java ofrece un soporte robusto para trabajar con ellos mediante las clases y métodos en su API. En este artículo, te guiaré paso a paso, desde los conceptos básicos hasta usos más avanzados, para que entiendas cómo trabajar con hilos en Java de manera efectiva.
¿Qué es un hilo?
Un hilo es la unidad más pequeña de procesamiento que puede ejecutarse de forma concurrente dentro de un programa. Cada hilo funciona de manera independiente, pero comparte recursos del programa principal, como la memoria.
Por defecto, todos los programas en Java tienen un hilo principal llamado main
, que se inicia automáticamente cuando ejecutas un programa.
Creación de hilos: Nivel básico
En Java, hay dos formas principales de crear y usar hilos:
1. Extender la clase Thread
La clase Thread
es la base de los hilos en Java. Puedes crear un hilo extendiendo esta clase y sobrescribiendo el método run()
.
class MiHilo extends Thread {
@Override
public void run() {
// Código que ejecutará el hilo
System.out.println("Hilo en ejecución: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MiHilo hilo1 = new MiHilo();
hilo1.start(); // Inicia el hilo
}
}
Notas:
- El método
run()
contiene el código que se ejecutará cuando el hilo inicie. - El método
start()
pone al hilo en estado «runnable», listo para ejecutarse.
2. Implementar la interfaz Runnable
Esta es la forma preferida cuando necesitas que tu clase herede de otra clase, ya que Java no permite la herencia múltiple.
class MiTarea implements Runnable {
@Override
public void run() {
System.out.println("Tarea en ejecución: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread hilo1 = new Thread(new MiTarea());
hilo1.start(); // Inicia el hilo
}
}
Diferencias principales:
Thread
es una clase, mientras queRunnable
es una interfaz.- Usar
Runnable
te da más flexibilidad si necesitas heredar de otra clase.
Sincronización: Evitando conflictos entre hilos
Cuando varios hilos acceden a recursos compartidos, como una variable o una lista, pueden producirse conflictos (condiciones de carrera). Para evitar esto, usamos sincronización.
Uso de synchronized
El modificador synchronized
garantiza que solo un hilo pueda ejecutar un bloque de código crítico a la vez.
class Contador {
private int valor = 0;
public synchronized void incrementar() {
valor++;
System.out.println(Thread.currentThread().getName() + " - Valor: " + valor);
}
}
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();
}
}
Comunicación entre hilos
Los hilos pueden necesitar coordinarse entre sí, por ejemplo, cuando uno debe esperar a que otro complete una tarea. Para esto usamos wait()
, notify()
y notifyAll()
.
Ejemplo: Productor-Consumidor
class Almacen {
private int producto = -1;
private boolean disponible = false;
public synchronized void producir(int valor) throws InterruptedException {
while (disponible) {
wait(); // Espera si ya hay un producto
}
producto = valor;
disponible = true;
System.out.println("Producido: " + producto);
notify(); // Notifica al consumidor
}
public synchronized int consumir() throws InterruptedException {
while (!disponible) {
wait(); // Espera si no hay producto disponible
}
disponible = false;
System.out.println("Consumido: " + producto);
notify(); // Notifica al productor
return producto;
}
}
public class Main {
public static void main(String[] args) {
Almacen almacen = new Almacen();
Thread productor = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
almacen.producir(i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumidor = new Thread(() -> {
try {
for (int i = 0; i < 5; i++) {
almacen.consumir();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
productor.start();
consumidor.start();
}
}
Uso de Executors
: Gestión avanzada de hilos
Cuando necesitas gestionar múltiples hilos, puedes usar el framework ExecutorService
. Esto facilita la creación, ejecución y finalización de hilos.
Ejemplo: Pool 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); // 3 hilos en el pool
for (int i = 1; i <= 10; i++) {
int tarea = i;
ejecutor.execute(() -> {
System.out.println("Ejecutando tarea " + tarea + " en " + Thread.currentThread().getName());
});
}
ejecutor.shutdown(); // Cierra el pool de hilos
}
}
Ventajas de ExecutorService
:
- Maneja automáticamente la reutilización de hilos.
- Facilita la ejecución de tareas concurrentes.
- Proporciona métodos para controlar el ciclo de vida de los hilos.
Resumen: Hilos en Java
- Conceptos básicos:
- Los hilos permiten ejecutar tareas concurrentemente.
- Puedes crear hilos extendiendo
Thread
o implementandoRunnable
.
- Sincronización:
- Usa
synchronized
para evitar conflictos al compartir recursos. wait()
,notify()
ynotifyAll()
son útiles para coordinar hilos.
- Usa
- Avanzado:
ExecutorService
simplifica la gestión de múltiples hilos.- Herramientas adicionales como
Callable
,Future
yCompletableFuture
permiten manejar tareas más complejas.
Con esta guía tienes los fundamentos para trabajar con hilos en Java, desde lo más básico hasta técnicas más avanzadas. ¡Ahora puedes comenzar a implementar tus propios sistemas concurrentes!