Ejecutores newCachedThreadPool () vs newFixedThreadPool ()

1. Información general

Cuando se trata de implementaciones de grupos de subprocesos, la biblioteca estándar de Java ofrece muchas opciones para elegir. Los grupos de subprocesos fijos y en caché son bastante ubicuos entre esas implementaciones.

En este tutorial, veremos cómo funcionan los grupos de subprocesos bajo el capó y luego compararemos estas implementaciones y sus casos de uso.

2. Grupo de subprocesos en caché

Echemos un vistazo a cómo Java crea un grupo de subprocesos en caché cuando llamamos Executors.newCachedThreadPool () :

public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }

Los grupos de subprocesos almacenados en caché utilizan el "traspaso síncrono" para poner en cola nuevas tareas. La idea básica de la transferencia síncrona es simple y, sin embargo, contraria a la intuición: se puede poner en cola un elemento si y solo si otro hilo toma ese elemento al mismo tiempo. En otras palabras, la SynchronousQueue no puede sostener cualquier tarea que sea.

Supongamos que entra una nueva tarea. Si hay un subproceso inactivo esperando en la cola, entonces el productor de la tarea entrega la tarea a ese subproceso. De lo contrario, dado que la cola siempre está llena, el ejecutor crea un nuevo hilo para manejar esa tarea .

El grupo almacenado en caché comienza con cero subprocesos y potencialmente puede crecer para tener subprocesos Integer.MAX_VALUE . Prácticamente, la única limitación para un grupo de subprocesos en caché son los recursos del sistema disponibles.

Para administrar mejor los recursos del sistema, los grupos de subprocesos almacenados en caché eliminarán los subprocesos que permanecen inactivos durante un minuto.

2.1. Casos de uso

La configuración del grupo de subprocesos en caché almacena en caché los subprocesos (de ahí el nombre) durante un corto período de tiempo para reutilizarlos para otras tareas. Como resultado, funciona mejor cuando se trata de un número razonable de tareas de corta duración.

La clave aquí es "razonable" y "de corta duración". Para aclarar este punto, evaluemos un escenario donde los grupos almacenados en caché no son una buena opción. Aquí vamos a enviar un millón de tareas, cada una de las cuales tarda 100 microsegundos en finalizar:

Callable task = () -> { long oneHundredMicroSeconds = 100_000; long startedAt = System.nanoTime(); while (System.nanoTime() - startedAt  task).collect(toList()); var result = cachedPool.invokeAll(tasks);

Esto creará una gran cantidad de subprocesos que se traducirán en un uso de memoria irrazonable y, lo que es peor, muchos cambios de contexto de CPU. Ambas anomalías dañarían significativamente el rendimiento general.

Por lo tanto, debemos evitar este grupo de subprocesos cuando el tiempo de ejecución es impredecible, como las tareas vinculadas a IO.

3. Grupo de subprocesos fijos

Veamos cómo funcionan los grupos de subprocesos fijos bajo el capó:

public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); }

A diferencia del grupo de subprocesos en caché, este utiliza una cola ilimitada con un número fijo de subprocesos que nunca caducan . Por lo tanto, en lugar de un número cada vez mayor de subprocesos, el grupo de subprocesos fijo intenta ejecutar tareas entrantes con una cantidad fija de subprocesos . Cuando todos los hilos están ocupados, el ejecutor pondrá en cola nuevas tareas. De esta manera, tenemos más control sobre el consumo de recursos de nuestro programa.

Como resultado, los grupos de subprocesos fijos son más adecuados para tareas con tiempos de ejecución impredecibles.

4. Similitudes desafortunadas

Hasta ahora, solo hemos enumerado las diferencias entre los grupos de subprocesos en caché y fijos.

Dejando de lado todas esas diferencias, ambos usan AbortPolicy como su política de saturación . Por lo tanto, esperamos que estos ejecutores generen una excepción cuando no puedan aceptar e incluso poner en cola más tareas.

Veamos qué pasa en el mundo real.

Los grupos de subprocesos almacenados en caché continuarán creando más y más subprocesos en circunstancias extremas, por lo que, prácticamente, nunca llegarán a un punto de saturación . De manera similar, los grupos de subprocesos fijos continuarán agregando más y más tareas en su cola. Por lo tanto, las piscinas fijas tampoco alcanzarán nunca un punto de saturación .

Como ambos grupos no se saturarán, cuando la carga es excepcionalmente alta, consumirán mucha memoria para crear subprocesos o tareas en cola. Para colmo de males, los grupos de subprocesos almacenados en caché también incurrirán en muchos cambios de contexto del procesador.

De todos modos, para tener más control sobre el consumo de recursos, es muy recomendable crear un ThreadPoolExecutor personalizado :

var boundedQueue = new ArrayBlockingQueue(1000); new ThreadPoolExecutor(10, 20, 60, SECONDS, boundedQueue, new AbortPolicy()); 

Aquí, nuestro grupo de subprocesos puede tener hasta 20 subprocesos y solo puede poner en cola hasta 1000 tareas. Además, cuando no pueda aceptar más carga, simplemente lanzará una excepción.

5. Conclusión

En este tutorial, echamos un vistazo al código fuente de JDK para ver cómo funcionan los diferentes Ejecutores bajo el capó. Luego, comparamos los grupos de subprocesos fijos y en caché y sus casos de uso.

Al final, intentamos abordar el consumo de recursos fuera de control de esos grupos con grupos de subprocesos personalizados.