Introducción a la concurrencia y el multihilo en Java
Las aplicaciones modernas requieren cada vez más la ejecución simultánea de varias tareas. Ya sea el procesamiento de solicitudes en un servidor, la descarga de archivos o cálculos complejos, no se puede prescindir de los mecanismos de concurrencia y multihilo. En Java, estos conceptos están integrados a nivel del lenguaje y de la biblioteca estándar, lo que lo convierte en una de las mejores herramientas para crear aplicaciones de alto rendimiento.
En este artículo, analizaremos todo, desde los conceptos básicos hasta las herramientas avanzadas: cómo crear hilos, sincronizar el acceso a los datos y utilizar utilidades modernas del paquete java.util.concurrent. El material será útil tanto para principiantes como para desarrolladores que deseen sistematizar sus conocimientos.
¿Qué es un hilo (Thread) y para qué sirve?
Un hilo es un proceso ligero que se ejecuta dentro de una aplicación. Cada programa Java tiene al menos un hilo (main). El multihilo permite ejecutar varios hilos simultáneamente, lo que es especialmente importante en procesadores multinúcleo.
Principales ventajas del multihilo:
- Mayor rendimiento gracias a la ejecución paralela de tareas.
- Mejora de la capacidad de respuesta de la interfaz de usuario.
- Uso eficiente de los recursos del sistema.
1. Creación de hilos en Java: enfoques clásicos
En Java, existen dos formas básicas de crear un hilo: heredando de la clase Thread e implementando la interfaz Runnable. Veamos ambas.
Herencia de la clase Thread
La forma más sencilla es crear una subclase de Thread y sobrescribir el método run().
public class MyThread extends Thread { @Override public void run() { System.out.println("Hilo " + getName() + " iniciado."); }
public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); // Iniciamos el hilo }}Importante: llamar a start(), no a run(). run() ejecutará el código en el hilo actual, mientras que start() crea uno nuevo.
Implementación de la interfaz Runnable
Un enfoque más flexible es implementar Runnable y pasarlo al constructor de Thread.
public class Task implements Runnable { @Override public void run() { System.out.println("Tarea ejecutándose en el hilo: " + Thread.currentThread().getName()); }
public static void main(String[] args) { Task task = new Task(); Thread thread = new Thread(task); thread.start(); }}Este método es preferible porque no requiere herencia y permite que la clase extienda otras clases.
2. Sincronización y atomicidad: cómo evitar la condición de carrera
Cuando varios hilos acceden simultáneamente a datos compartidos, se produce una condición de carrera (race condition). Para evitarlo, Java proporciona mecanismos de sincronización.
La palabra clave synchronized
Con synchronized se puede bloquear un método o un bloque de código para un solo hilo.
public class Counter { private int count = 0;
public synchronized void increment() { count++; }
public int getCount() { return count; }}En este ejemplo, solo un hilo puede ejecutar increment() en un momento dado. Sin embargo, para operaciones simples como el incremento, es mejor usar clases atómicas.
Clases atómicas (Atomic)
El paquete java.util.concurrent.atomic contiene clases que proporcionan operaciones atómicas sin bloqueos.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicCounter { private A