Executors.newCachedThreadPool () versus Executors.newFixedThreadPool ()

Respuestas:

202

Creo que los documentos explican la diferencia y el uso de estas dos funciones bastante bien:

newFixedThreadPool

Crea un grupo de subprocesos que reutiliza un número fijo de subprocesos que operan en una cola compartida no acotada. En cualquier momento, a lo sumo, los hilos nThreads serán tareas de procesamiento activas. Si se envían tareas adicionales cuando todos los hilos están activos, esperarán en la cola hasta que haya un hilo disponible. Si algún subproceso termina debido a un error durante la ejecución antes del apagado, uno nuevo tomará su lugar si es necesario para ejecutar tareas posteriores. Los subprocesos en el grupo existirán hasta que se cierre explícitamente.

newCachedThreadPool

Crea un grupo de subprocesos que crea nuevos subprocesos según sea necesario, pero reutilizará los subprocesos construidos previamente cuando estén disponibles. Estos grupos generalmente mejorarán el rendimiento de los programas que ejecutan muchas tareas asincrónicas de corta duración. Las llamadas a ejecutar reutilizarán hilos previamente construidos si están disponibles. Si no hay un hilo existente disponible, se creará un nuevo hilo y se agregará al grupo. Los subprocesos que no se han utilizado durante sesenta segundos se terminan y se eliminan de la memoria caché. Por lo tanto, un grupo que permanezca inactivo durante el tiempo suficiente no consumirá ningún recurso. Tenga en cuenta que los grupos con propiedades similares pero detalles diferentes (por ejemplo, parámetros de tiempo de espera) se pueden crear utilizando los constructores ThreadPoolExecutor.

En términos de recursos, newFixedThreadPoolmantendrá todos los subprocesos en ejecución hasta que finalicen explícitamente. En elnewCachedThreadPool subprocesos que no se han utilizado durante sesenta segundos se terminan y se eliminan de la memoria caché.

Ante esto, el consumo de recursos dependerá mucho de la situación. Por ejemplo, si tiene una gran cantidad de tareas de larga ejecución, sugeriría el FixedThreadPool. En cuanto a la CachedThreadPool, los documentos dicen que "estos grupos generalmente mejorarán el rendimiento de los programas que ejecutan muchas tareas asincrónicas de corta duración".

bruno conde
fuente
1
sí, he revisado los documentos ... el problema es ... fixedThreadPool está causando un error de falta de memoria en 3 subprocesos ... mientras que cachedPool crea internamente solo un subproceso ... al aumentar el tamaño del montón, obtengo el mismo rendimiento para ambos ... ¿hay algo más que me estoy perdiendo!
hakish
1
¿Está proporcionando algún Threadfactory al ThreadPool? Supongo que podría estar almacenando algún estado en los hilos que no se está recolectando basura. De lo contrario, tal vez su programa se esté ejecutando tan cerca del tamaño límite de almacenamiento dinámico que con la creación de 3 subprocesos cause un OutOfMemory. Además, si cachedPool está creando internamente un solo subproceso, esto puede indicar que sus tareas se ejecutan sincronizadas.
bruno conde
@brunoconde Justo como @Louis F. señala que newCachedThreadPoolpodría causar algunos problemas serios porque deja todo el control al thread pooly cuando el servicio está trabajando con otros en el mismo host , lo que puede causar que los demás se bloqueen debido a la larga espera de la CPU. Así que creo que newFixedThreadPoolpuede ser más seguro en este tipo de escenario. También este poste aclara las diferencias más importantes entre ellos.
Hearen
75

Solo para completar las otras respuestas, me gustaría citar Effective Java, 2nd Edition, de Joshua Bloch, capítulo 10, Artículo 68:

"Elegir el servicio de ejecución para una aplicación en particular puede ser complicado. Si está escribiendo un programa pequeño o un servidor con poca carga , usar Executors.new- CachedThreadPool generalmente es una buena opción , ya que no requiere configuración y generalmente" cosa correcta." ¡Pero un grupo de subprocesos en caché no es una buena opción para un servidor de producción muy cargado !

En un grupo de subprocesos en caché , las tareas enviadas no se ponen en cola, sino que se transfieren inmediatamente a un subproceso para su ejecución. Si no hay hilos disponibles, se crea uno nuevo . Si un servidor está tan cargado que todas sus CPU se utilizan por completo y llegan más tareas, se crearán más subprocesos, lo que solo empeorará las cosas.

Por lo tanto, en un servidor de producción muy cargado , es mucho mejor usar Executors.newFixedThreadPool , que le brinda un grupo con un número fijo de subprocesos, o usar la clase ThreadPoolExecutor directamente, para un control máximo. "

Louis F.
fuente
15

Si observa el código fuente , verá que están llamando a ThreadPoolExecutor. internamente y estableciendo sus propiedades. Puede crear uno para tener un mejor control de sus requisitos.

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

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
}
krmanish007
fuente
1
Exactamente, un ejecutor de subprocesos en caché con un límite superior sensato y decir, 5-10 minutos de cosecha inactiva es perfecto para la mayoría de las ocasiones.
Agoston Horvath
12

Si no le preocupa una cola ilimitada de tareas ejecutables / ejecutables , puede usar una de ellas. Como sugirió bruno, yo también prefiero newFixedThreadPoola newCachedThreadPoolestos dos.

Pero ThreadPoolExecutor proporciona características más flexibles en comparación con cualquiera newFixedThreadPoolonewCachedThreadPool

ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, 
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)

Ventajas:

  1. Tienes control total del tamaño de BlockingQueue . No es ilimitado, a diferencia de las dos opciones anteriores. No obtendré un error de falta de memoria debido a una gran acumulación de tareas pendientes ejecutables / ejecutables cuando haya una turbulencia inesperada en el sistema.

  2. Puede implementar una política de manejo de rechazo personalizada O utilizar una de las políticas:

    1. En el valor predeterminado ThreadPoolExecutor.AbortPolicy, el controlador arroja un tiempo de ejecución RejectedExecutionException tras el rechazo.

    2. En ThreadPoolExecutor.CallerRunsPolicy, el hilo que invoca ejecutarse ejecuta la tarea. Esto proporciona un mecanismo de control de retroalimentación simple que reducirá la velocidad de envío de nuevas tareas.

    3. En ThreadPoolExecutor.DiscardPolicy, una tarea que no se puede ejecutar simplemente se descarta.

    4. En ThreadPoolExecutor.DiscardOldestPolicy, si el ejecutor no se cierra, la tarea en la cabecera de la cola de trabajo se descarta y luego se vuelve a intentar la ejecución (que puede fallar nuevamente y hacer que esto se repita).

  3. Puede implementar una fábrica de subprocesos personalizada para los siguientes casos de uso:

    1. Para establecer un nombre de hilo más descriptivo
    2. Para establecer el estado del demonio de hilo
    3. Para establecer la prioridad del hilo
Ravindra babu
fuente
11

Así es, Executors.newCachedThreadPool()no es una gran opción para el código de servidor que atiende a múltiples clientes y solicitudes concurrentes.

¿Por qué? Básicamente hay dos problemas (relacionados) con él:

  1. No tiene límites, lo que significa que está abriendo la puerta para que cualquier persona paralice su JVM simplemente inyectando más trabajo en el servicio (ataque DoS). Los subprocesos consumen una cantidad de memoria no despreciable y también aumentan el consumo de memoria en función de su trabajo en progreso, por lo que es bastante fácil derribar un servidor de esta manera (a menos que tenga otros interruptores en su lugar).

  2. El problema ilimitado se ve exacerbado por el hecho de que el Ejecutor está al frente de un SynchronousQueueque significa que hay una transferencia directa entre el encargado de la tarea y el grupo de subprocesos. Cada nueva tarea creará un nuevo hilo si todos los hilos existentes están ocupados. Esta es generalmente una mala estrategia para el código del servidor. Cuando la CPU se satura, las tareas existentes tardan más en finalizar. Sin embargo, se envían más tareas y se crean más hilos, por lo que las tareas tardan más y más en completarse. Cuando la CPU está saturada, más subprocesos definitivamente no es lo que necesita el servidor.

Aquí están mis recomendaciones:

Utilice un grupo de subprocesos de tamaño fijo Executors.newFixedThreadPool o un ThreadPoolExecutor. con un número máximo de hilos establecido;

Prashant Gautam
fuente
6

La ThreadPoolExecutorclase es la implementación base para los ejecutores que se devuelven de muchos de los Executorsmétodos de fábrica. Entonces acerquémonos a los grupos de subprocesos fijos y en caché deThreadPoolExecutor la perspectiva de.

ThreadPoolExecutor

El constructor principal de esta clase se ve así:

public ThreadPoolExecutor(
                  int corePoolSize,
                  int maximumPoolSize,
                  long keepAliveTime,
                  TimeUnit unit,
                  BlockingQueue<Runnable> workQueue,
                  ThreadFactory threadFactory,
                  RejectedExecutionHandler handler
)

Tamaño de la piscina central

El corePoolSizedetermina el tamaño mínimo de la agrupación de hebras diana.La implementación mantendría un grupo de ese tamaño incluso si no hay tareas para ejecutar.

Tamaño máximo de piscina

El maximumPoolSizees el número máximo de subprocesos que pueden estar activos a la vez.

Después de que el grupo de subprocesos crezca y se vuelva más grande que el corePoolSizeumbral, el ejecutor puede terminar los subprocesos inactivos y alcanzar el corePoolSizenuevo. Si allowCoreThreadTimeOutes cierto, entonces el ejecutor puede incluso terminar los subprocesos de la agrupación central si estuvieran inactivos por encima del keepAliveTimeumbral.

Por lo tanto, la conclusión es que si los subprocesos permanecen inactivos más allá del keepAliveTimeumbral, pueden terminarse ya que no hay demanda para ellos.

Haciendo cola

¿Qué sucede cuando entra una nueva tarea y todos los hilos centrales están ocupados? Las nuevas tareas se pondrán en cola dentro de esa BlockingQueue<Runnable>instancia. Cuando un hilo se libera, se puede procesar una de esas tareas en cola.

Existen diferentes implementaciones de la BlockingQueueinterfaz en Java, por lo que podemos implementar diferentes enfoques de colas como:

  1. Cola limitada : las nuevas tareas se pondrían en cola dentro de una cola de tareas limitada.

  2. Cola sin límites : las nuevas tareas se pondrían en cola dentro de una cola de tareas sin límites. Por lo tanto, esta cola puede crecer tanto como lo permita el tamaño del almacenamiento dinámico.

  3. Traspaso sincrónico : también podemos usar el SynchronousQueuepara poner en cola las nuevas tareas. En ese caso, al poner en cola una nueva tarea, otro hilo ya debe estar esperando esa tarea.

Sumisión de trabajo

Así es como ThreadPoolExecutorejecuta una nueva tarea:

  1. Si menos de corePoolSize están ejecutando subprocesos, intenta iniciar un nuevo subproceso con la tarea dada como primer trabajo.
  2. De lo contrario, intenta poner en cola la nueva tarea utilizando el BlockingQueue#offermétodo El offermétodo no se bloqueará si la cola está llena e inmediatamente regresa false.
  3. Si no puede poner en cola la nueva tarea (es decir, offerretorna false), intenta agregar un nuevo subproceso al grupo de subprocesos con esta tarea como su primer trabajo.
  4. Si no puede agregar el nuevo hilo, entonces el ejecutor se apaga o se satura. De cualquier manera, la nueva tarea sería rechazada utilizando la proporcionada RejectedExecutionHandler.

La principal diferencia entre los grupos de subprocesos fijos y en caché se reduce a estos tres factores:

  1. Tamaño de la piscina central
  2. Tamaño máximo de piscina
  3. Haciendo cola
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
El | Tipo de piscina | Tamaño del núcleo | Tamaño máximo | Estrategia de cola |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
El | Fijo | n (fijo) | n (fijo) | Sin límites `LinkedBlockingQueue` |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +
El | En caché | 0 | Integer.MAX_VALUE | `SynchronousQueue` |
+ ----------- + ----------- + ------------------- + ----- ---------------------------- +


Grupo de subprocesos fijos


Así es como Excutors.newFixedThreadPool(n)funciona:

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

Como puedes ver:

  • El tamaño del grupo de subprocesos es fijo.
  • Si hay una gran demanda, no crecerá.
  • Si los hilos están inactivos durante bastante tiempo, no se encogerá.
  • Supongamos que todos esos hilos están ocupados con algunas tareas de larga duración y la tasa de llegada sigue siendo bastante alta. Como el ejecutor usa una cola ilimitada, puede consumir una gran parte del montón. Siendo desafortunado, podemos experimentar una OutOfMemoryError.

¿Cuándo debo usar uno u otro? ¿Qué estrategia es mejor en términos de utilización de recursos?

Un grupo de subprocesos de tamaño fijo parece ser un buen candidato cuando vamos a limitar el número de tareas concurrentes para fines de gestión de recursos .

Por ejemplo, si vamos a usar un ejecutor para manejar las solicitudes del servidor web, un ejecutor fijo puede manejar las ráfagas de solicitudes de manera más razonable.

Para una mejor gestión de los recursos, es muy recomendable crear una aplicación personalizada ThreadPoolExecutorcon una BlockingQueue<T>implementación limitada y razonable RejectedExecutionHandler.


Grupo de subprocesos en caché


Así es como Executors.newCachedThreadPool()funciona:

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

Como puedes ver:

  • El grupo de subprocesos puede crecer de cero subprocesos a Integer.MAX_VALUE. Prácticamente, el grupo de subprocesos no tiene límites.
  • Si algún subproceso está inactivo durante más de 1 minuto, puede terminar. Por lo tanto, el grupo puede reducirse si los hilos permanecen demasiado inactivos.
  • Si todos los hilos asignados están ocupados mientras entra una nueva tarea, entonces se crea un nuevo hilo, ya que ofrecer una nueva tarea a un SynchronousQueuesiempre falla cuando no hay nadie en el otro extremo que lo acepte.

¿Cuándo debo usar uno u otro? ¿Qué estrategia es mejor en términos de utilización de recursos?

Úselo cuando tenga muchas tareas predecibles de corta duración.

Ali Dehghani
fuente
5

Debe usar newCachedThreadPool solo cuando tenga tareas asincrónicas de corta duración como se indica en Javadoc, si envía tareas que requieren más tiempo para procesar, terminará creando demasiados hilos. Puede alcanzar el 100% de la CPU si envía tareas de ejecución prolongada a un ritmo más rápido a newCachedThreadPool ( http://rashcoder.com/be-careful-while-using-executors-newcachedthreadpool/ ).

Dhanaraj Durairaj
fuente
1

Hago algunas pruebas rápidas y tengo los siguientes hallazgos:

1) si usa SynchronousQueue:

Después de que los subprocesos alcancen el tamaño máximo, cualquier trabajo nuevo será rechazado con la excepción que se muestra a continuación.

Excepción en el subproceso "principal" java.util.concurrent.RejectedExecutionException: Tarea java.util.concurrent.FutureTask@3fee733d rechazada de java.util.concurrent.ThreadPoolExecutor@5acf9800 [Ejecutando, tamaño de grupo = 3, subprocesos activos = 3, tareas en cola = 3, tareas en cola = 3 = 0, tareas completadas = 0]

en java.util.concurrent.ThreadPoolExecutor $ AbortPolicy.rejectedExecution (ThreadPoolExecutor.java:2047)

2) si usa LinkedBlockingQueue:

Los subprocesos nunca aumentan del tamaño mínimo al tamaño máximo, lo que significa que el grupo de subprocesos tiene un tamaño fijo como el tamaño mínimo.

Mike Lin
fuente