Introducción a Thread Pools en Java

Un thread pool es la aplicación del patrón de diseño pool object a hilos. Aunque este no es el tutorial para aprender a usar este patrón, hago una introducción mostrando cómo funciona un thread pool, que nos ayuda a economizar la creación de hilos en Java.

Crear muchos hilos en un programa puede parecer una solución sencilla para manejar múltiples tareas concurrentes, pero en realidad es una práctica que puede afectar negativamente al rendimiento de nuestro ordenador. Cada hilo que creamos consume recursos del sistema operativo: hay que reservar memoria, gestionar su ciclo de vida, y además, todos los hilos compiten por el tiempo de CPU disponible. Esto puede provocar que, aunque nuestro programa funcione aparentemente bien, el sistema se ralentice o se degrade la experiencia de uso.

Si pensamos en un escenario típico, como un servidor web que recibe peticiones de clientes, crear un hilo nuevo para cada petición puede ser viable cuando el volumen es bajo. Pero si de repente llegan cientos de peticiones por segundo, crear un hilo para cada una puede saturar la máquina virtual y el sistema operativo, ya que habrá demasiados hilos compitiendo por recursos limitados. Esto no solo consume más memoria y CPU, sino que también genera una sobrecarga en la gestión de los hilos, que el sistema debe alternar constantemente para que todos avancen.

Para evitar estos problemas, Java ofrece una solución basada en el concepto de thread pool, que es fundamental para entender cómo funcionan los ejecutores (Executor). Un thread pool es un contenedor o banco de hilos preinstanciados que se reutilizan para ejecutar múltiples tareas. En lugar de crear un hilo nuevo cada vez que llega una tarea, el thread pool mantiene un conjunto limitado de hilos listos para ser usados.

Imaginemos que tenemos una CPU con 8 núcleos. Lo habitual es que el thread pool cree un número de hilos similar a la cantidad de núcleos, por ejemplo 8. Cuando llega una tarea, esta se encola y espera a que uno de esos hilos esté disponible. Cuando un hilo termina de ejecutar una tarea, no se destruye, sino que vuelve al pool para ser reutilizado con la siguiente tarea que llegue. Esto evita la sobrecarga de crear y destruir hilos constantemente y mejora la eficiencia del sistema.

Si en algún momento todos los hilos están ocupados y llegan más tareas, estas se quedan en cola esperando a que un hilo se libere. Algunos thread pools pueden crecer dinámicamente hasta un límite máximo, creando más hilos si la demanda lo requiere, pero siempre con un tope para no saturar el sistema. Además, si un hilo lleva mucho tiempo sin usarse, el pool puede decidir eliminarlo para liberar recursos.

Este enfoque es similar a tener varias cajas en un supermercado con varios cajeros. Si hay pocos clientes, solo se usan algunas cajas, y si hay mucha afluencia, se abren más cajas para atender a todos. Pero nunca se abre una caja para cada cliente, porque sería ineficiente y ocuparía demasiado espacio.

En la práctica, cuando usamos un Executor en Java, estamos trabajando con un thread pool que gestiona estos hilos reutilizables. Por ejemplo, si configuramos un ejecutor con 10 hilos, lo que estamos haciendo es crear un pool con 10 workers que atenderán las tareas que enviemos. Si enviamos cinco tareas, puede que todas se ejecuten en el mismo hilo, uno tras otro, porque el pool reutiliza los hilos disponibles.

No necesitamos preocuparnos por cómo exactamente se asignan las tareas a los hilos ni cómo se gestionan internamente, ya que la API de alto nivel de Java abstrae esos detalles. Lo importante es entender que detrás de esa simplicidad hay un mecanismo eficiente que evita crear hilos de forma indiscriminada y que mejora el rendimiento y la estabilidad de nuestras aplicaciones concurrentes.

Para ilustrar este concepto, podemos imaginar un código sencillo donde creamos un ExecutorService con un pool fijo de hilos y enviamos varias tareas para que se ejecuten:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // Creamos un thread pool con 4 hilos
        ExecutorService executor = Executors.newFixedThreadPool(4);

        // Enviamos 10 tareas para que se ejecuten
        for (int i = 1; i <= 10; i++) {
            int taskNumber = i;
            executor.submit(() -> {
                System.out.println("Ejecutando tarea " + taskNumber + " en hilo " + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000); // Simula trabajo
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // Cerramos el executor cuando terminen las tareas
        executor.shutdown();
    }
}

En este ejemplo, aunque enviamos 10 tareas, solo se crearán 4 hilos que se irán reutilizando para ejecutar todas las tareas. Esto es mucho más eficiente que crear 10 hilos nuevos, uno por cada tarea.

En definitiva, el uso de thread pools es una práctica recomendada para gestionar tareas concurrentes en Java, ya que optimiza el uso de recursos, mejora el rendimiento y evita problemas derivados de crear demasiados hilos. Al entender este concepto, podemos aprovechar mejor las herramientas que nos ofrece Java para construir aplicaciones concurrentes robustas y eficientes.

Lista de reproducción
  1. 1
    ¿Qué es la concurrencia?
    8 minutos
  2. 2
    ¿Qué es un hilo?
    5 minutos
  3. 3
    Crear un hilo en Java
    10 minutos
  4. 4
    Interrupción de hilos
    10 minutos
  5. 5
    Cómo usar Thread.join
    14 minutos
  6. 6
    Corrupción de memoria
    8 minutos
  7. 7
    Monitores y synchronized
    10 minutos
  8. 8
    Bloque synchronized
    12 minutos
  9. 9
    Preguntas típicas sobre synchronized en Java
    8 minutos
  10. 10
    Interbloqueos, synchronized y el problema de la cena de los filósofos
    13 minutos
  11. 11
    Introducción a Executor en Java
    11 minutos
  12. 12
    Introducción al uso de ExecutorService en Java
    14 minutos
  13. 13
    Introducción a Thread Pools en Java
    10 minutos
  14. 14
    Cómo crear Thread Pools en Java
    11 minutos
  15. 15
    ¿Cómo funcionan wait() y notify()?
    10 minutos
  16. 16
    Ejemplo de wait() y notify() en Java
    9 minutos
  17. 17
    La palabra clave volatile
    8 minutos
  18. 18
    Cómo usar Future
    8 minutos
  19. 19
    Variables atómicas
    7 minutos