Estos apuntes te guiarán a través de los conceptos esenciales de los hilos en Java, desde los fundamentos hasta ejemplos avanzados, asegurándote una comprensión sólida para abordar cualquier tarea relacionada con programación concurrente.
1. Conceptos Básicos sobre Hilos
Un hilo (thread) es una unidad de ejecución que puede ejecutarse de manera independiente dentro de un programa. Los hilos permiten realizar múltiples tareas de forma concurrente, aprovechando mejor los recursos del sistema.
Ciclo de Vida de un Hilo
- Nuevo (New): Se crea el hilo (
new Thread()
), pero no está ejecutándose. - Ejecutable (Runnable): El hilo está listo para ejecutarse tras llamar a
start()
, pero espera ser programado por el sistema operativo. - Ejecutando (Running): El hilo está en ejecución.
- Bloqueado/En espera (Blocked/Waiting): El hilo espera por un recurso o una condición para continuar.
- Terminado (Terminated): El hilo ha completado su ejecución.
2. Cómo Crear y Usar Hilos en Java
2.1 Extender la clase Thread
Esta es una forma directa de crear hilos. Sobrescribes el método run()
para definir la tarea que el hilo ejecutará.
class MiHilo extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " ejecutando iteración " + i);
}
}
}
public class Main {
public static void main(String[] args) {
MiHilo hilo1 = new MiHilo();
hilo1.start(); // Inicia el hilo
}
}
run()
: Contiene el código que ejecutará el hilo.start()
: Llama internamente arun()
y permite que el hilo se ejecute concurrentemente.
2.2 Implementar la Interfaz Runnable
Esta técnica es más flexible, ya que Java no permite la herencia múltiple.
class MiTarea implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " ejecutando iteración " + i);
}
}
}
public class Main {
public static void main(String[] args) {
Thread hilo = new Thread(new MiTarea());
hilo.start(); // Inicia el hilo
}
}
Diferencias entre Thread
y Runnable
:
- Extender
Thread
es útil para casos simples. - Implementar
Runnable
es preferible cuando necesitas heredar de otra clase.
3. Gestión de Recursos Compartidos
Cuando varios hilos acceden a un recurso compartido (como una variable), pueden ocurrir problemas de condiciones de carrera. Para evitarlas, usamos sincronización.
3.1 Uso de synchronized
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();
}
}
3.2 Bloques Síncronos
Puedes sincronizar solo una sección del código crítico en lugar de todo el método.
public void incrementar() {
synchronized (this) {
// Sección crítica
valor++;
System.out.println(Thread.currentThread().getName() + " - Valor: " + valor);
}
}
4. Coordinación entre Hilos
4.1 Métodos wait()
y notify()
Estos métodos permiten que los hilos se coordinen cuando comparten recursos. Deben ser usados dentro de bloques sincronizados.
Ejemplo: Productor-Consumidor
class Almacen {
private int producto;
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 = 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 = 1; i <= 5; i++) {
almacen.producir(i);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
Thread consumidor = new Thread(() -> {
try {
for (int i = 1; i <= 5; i++) {
almacen.consumir();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
productor.start();
consumidor.start();
}
}
5. Uso de Ejecutores (ExecutorService
)
Cuando necesitas gestionar muchos hilos, ExecutorService
simplifica el proceso. Permite crear grupos de hilos (pools) y gestionar su ejecución.
Ejemplo de 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); // Pool de 3 hilos
for (int i = 1; i <= 10; i++) {
final int tarea = i;
ejecutor.execute(() -> {
System.out.println("Tarea " + tarea + " ejecutada por " + Thread.currentThread().getName());
});
}
ejecutor.shutdown(); // Cierra el pool de hilos
}
}
6. Errores Comunes al Trabajar con Hilos
- Usar
run()
en lugar destart()
:- Esto ejecuta el código en el hilo principal, no en un nuevo hilo.
- Falta de sincronización:
- Sin mecanismos de sincronización, los datos compartidos pueden volverse inconsistentes.
- Bloqueos (Deadlocks):
- Ocurren cuando dos o más hilos esperan indefinidamente por recursos que el otro posee.
- No manejar interrupciones:
- Los hilos deben manejar correctamente interrupciones usando
Thread.currentThread().interrupt()
.
- Los hilos deben manejar correctamente interrupciones usando
Resumen
- Hilos Básicos:
- Crear hilos extendiendo
Thread
o implementandoRunnable
.
- Crear hilos extendiendo
- Sincronización:
- Usa
synchronized
para evitar condiciones de carrera.
- Usa
- Coordinación:
- Métodos
wait()
ynotify()
son clave para coordinar hilos.
- Métodos
- Gestión Avanzada:
- Usa
ExecutorService
para manejar grandes cantidades de hilos.
- Usa
Estos apuntes te ayudarán a comprender y aplicar los conceptos esenciales sobre hilos en Java. Practica con ejemplos similares para reforzar tu conocimiento.