newCachedThreadPool()
versus newFixedThreadPool()
¿Cuándo debo usar uno u otro? ¿Qué estrategia es mejor en términos de utilización de recursos?
newCachedThreadPool()
versus newFixedThreadPool()
¿Cuándo debo usar uno u otro? ¿Qué estrategia es mejor en términos de utilización de recursos?
Respuestas:
Creo que los documentos explican la diferencia y el uso de estas dos funciones bastante bien:
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.
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, newFixedThreadPool
mantendrá 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".
newCachedThreadPool
podría causar algunos problemas serios porque deja todo el control al thread pool
y 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 newFixedThreadPool
puede ser más seguro en este tipo de escenario. También este poste aclara las diferencias más importantes entre ellos.
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. "
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>());
}
Si no le preocupa una cola ilimitada de tareas ejecutables / ejecutables , puede usar una de ellas. Como sugirió bruno, yo también prefiero newFixedThreadPool
a newCachedThreadPool
estos dos.
Pero ThreadPoolExecutor proporciona características más flexibles en comparación con cualquiera newFixedThreadPool
onewCachedThreadPool
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)
Ventajas:
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.
Puede implementar una política de manejo de rechazo personalizada O utilizar una de las políticas:
En el valor predeterminado ThreadPoolExecutor.AbortPolicy
, el controlador arroja un tiempo de ejecución RejectedExecutionException tras el rechazo.
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.
En ThreadPoolExecutor.DiscardPolicy
, una tarea que no se puede ejecutar simplemente se descarta.
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).
Puede implementar una fábrica de subprocesos personalizada para los siguientes casos de uso:
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:
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).
El problema ilimitado se ve exacerbado por el hecho de que el Ejecutor está al frente de un SynchronousQueue
que 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;
La ThreadPoolExecutor
clase es la implementación base para los ejecutores que se devuelven de muchos de los Executors
métodos de fábrica. Entonces acerquémonos a los grupos de subprocesos fijos y en caché deThreadPoolExecutor
la perspectiva de.
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
)
El corePoolSize
determina 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.
El maximumPoolSize
es 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 corePoolSize
umbral, el ejecutor puede terminar los subprocesos inactivos y alcanzar el corePoolSize
nuevo. Si allowCoreThreadTimeOut
es cierto, entonces el ejecutor puede incluso terminar los subprocesos de la agrupación central si estuvieran inactivos por encima del keepAliveTime
umbral.
Por lo tanto, la conclusión es que si los subprocesos permanecen inactivos más allá del keepAliveTime
umbral, pueden terminarse ya que no hay demanda para ellos.
¿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 BlockingQueue
interfaz en Java, por lo que podemos implementar diferentes enfoques de colas como:
Cola limitada : las nuevas tareas se pondrían en cola dentro de una cola de tareas limitada.
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.
Traspaso sincrónico : también podemos usar el SynchronousQueue
para poner en cola las nuevas tareas. En ese caso, al poner en cola una nueva tarea, otro hilo ya debe estar esperando esa tarea.
Así es como ThreadPoolExecutor
ejecuta una nueva tarea:
corePoolSize
están ejecutando subprocesos, intenta iniciar un nuevo subproceso con la tarea dada como primer trabajo.BlockingQueue#offer
método El offer
método no se bloqueará si la cola está llena e inmediatamente regresa false
.offer
retorna false
), intenta agregar un nuevo subproceso al grupo de subprocesos con esta tarea como su primer trabajo.RejectedExecutionHandler
.La principal diferencia entre los grupos de subprocesos fijos y en caché se reduce a estos tres factores:
+ ----------- + ----------- + ------------------- + ----- ---------------------------- + 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` | + ----------- + ----------- + ------------------- + ----- ---------------------------- +
Excutors.newFixedThreadPool(n)
funciona:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Como puedes ver:
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 ThreadPoolExecutor
con una BlockingQueue<T>
implementación limitada y razonable RejectedExecutionHandler
.
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:
Integer.MAX_VALUE
. Prácticamente, el grupo de subprocesos no tiene límites.SynchronousQueue
siempre 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.
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/ ).
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.