newCachedThreadPool()
contre newFixedThreadPool()
Quand dois-je utiliser l'un ou l'autre? Quelle stratégie est la meilleure en termes d'utilisation des ressources?
newCachedThreadPool()
contre newFixedThreadPool()
Quand dois-je utiliser l'un ou l'autre? Quelle stratégie est la meilleure en termes d'utilisation des ressources?
Réponses:
Je pense que la documentation explique assez bien la différence et l'utilisation de ces deux fonctions:
Crée un pool de threads qui réutilise un nombre fixe de threads fonctionnant à partir d'une file d'attente partagée illimitée. À tout moment, la plupart des threads nThreads seront des tâches de traitement actives. Si des tâches supplémentaires sont soumises lorsque tous les threads sont actifs, ils attendront dans la file d'attente jusqu'à ce qu'un thread soit disponible. Si un thread se termine en raison d'un échec lors de l'exécution avant l'arrêt, un nouveau prendra sa place si nécessaire pour exécuter les tâches suivantes. Les threads du pool existeront jusqu'à ce qu'il soit explicitement arrêté.
Crée un pool de threads qui crée de nouveaux threads selon les besoins, mais réutilise les threads précédemment construits lorsqu'ils sont disponibles. Ces pools améliorent généralement les performances des programmes qui exécutent de nombreuses tâches asynchrones de courte durée. Les appels à exécuter réutiliseront les threads précédemment construits s'ils sont disponibles. Si aucun thread existant n'est disponible, un nouveau thread sera créé et ajouté au pool. Les threads qui n'ont pas été utilisés pendant soixante secondes sont arrêtés et supprimés du cache. Ainsi, un pool qui reste inactif suffisamment longtemps ne consommera aucune ressource. Notez que des pools avec des propriétés similaires mais des détails différents (par exemple, des paramètres de délai d'expiration) peuvent être créés à l'aide des constructeurs ThreadPoolExecutor.
En termes de ressources, le newFixedThreadPool
maintiendra tous les threads en cours d'exécution jusqu'à ce qu'ils soient explicitement terminés. Dans les newCachedThreadPool
threads qui n'ont pas été utilisés pendant soixante secondes sont arrêtés et supprimés du cache.
Compte tenu de cela, la consommation de ressources dépendra beaucoup de la situation. Par exemple, si vous avez un grand nombre de tâches de longue durée, je suggérerais le FixedThreadPool
. En ce qui concerne le CachedThreadPool
, la documentation dit que "ces pools amélioreront généralement les performances des programmes qui exécutent de nombreuses tâches asynchrones de courte durée".
newCachedThreadPool
peut causer de sérieux problèmes car vous laissez tout le contrôle au thread pool
et lorsque le service fonctionne avec d'autres dans le même hôte , ce qui peut provoquer un crash des autres en raison d'une attente prolongée du processeur. Je pense donc que cela newFixedThreadPool
peut être plus sûr dans ce genre de scénario. Cet article clarifie également les différences les plus marquantes entre eux.
Pour compléter les autres réponses, je voudrais citer Effective Java, 2e édition, par Joshua Bloch, chapitre 10, point 68:
"Le choix du service exécuteur pour une application particulière peut être délicat. Si vous écrivez un petit programme ou un serveur peu chargé , utiliser Executors.new- CachedThreadPool est généralement un bon choix , car il ne nécessite aucune configuration et généralement" bonne chose." Mais un pool de threads mis en cache n'est pas un bon choix pour un serveur de production fortement chargé !
Dans un pool de threads mis en cache , les tâches soumises ne sont pas mises en file d'attente mais immédiatement transmises à un thread pour exécution. Si aucun thread n'est disponible, un nouveau est créé . Si un serveur est tellement chargé que tous ses processeurs sont pleinement utilisés et que davantage de tâches arrivent, plus de threads seront créés, ce qui ne fera qu'empirer les choses.
Par conséquent, dans un serveur de production très chargé , il est préférable d'utiliser Executors.newFixedThreadPool , qui vous donne un pool avec un nombre fixe de threads, ou d'utiliser directement la classe ThreadPoolExecutor, pour un contrôle maximal. "
Si vous regardez le code source , vous verrez, ils appellent ThreadPoolExecutor. en interne et définir leurs propriétés. Vous pouvez créer le vôtre pour avoir une meilleure maîtrise de vos besoins.
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 vous n'êtes pas préoccupé par une file d'attente illimitée de tâches appelables / exécutables , vous pouvez utiliser l'une d'entre elles. Comme suggéré par bruno, je préfère aussi newFixedThreadPool
à newCachedThreadPool
plus de ces deux.
Mais ThreadPoolExecutor offre des fonctionnalités plus flexibles par rapport à l'un newFixedThreadPool
ou l' autrenewCachedThreadPool
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)
Avantages:
Vous avez un contrôle total sur la taille de BlockingQueue . Ce n'est pas illimité, contrairement aux deux options précédentes. Je n'obtiendrai pas une erreur de mémoire insuffisante en raison d'un énorme tas de tâches appelables / exécutables en attente lorsqu'il y a des turbulences inattendues dans le système.
Vous pouvez mettre en œuvre une stratégie de gestion des rejets personnalisée OU utiliser l'une des stratégies:
Par défaut ThreadPoolExecutor.AbortPolicy
, le gestionnaire lève une exception RejectedExecutionException lors du rejet.
Dans ThreadPoolExecutor.CallerRunsPolicy
, le thread qui invoque s'exécute lui-même exécute la tâche. Cela fournit un mécanisme de contrôle de rétroaction simple qui ralentira la vitesse à laquelle de nouvelles tâches sont soumises.
Dans ThreadPoolExecutor.DiscardPolicy
, une tâche qui ne peut pas être exécutée est simplement supprimée.
Dans ThreadPoolExecutor.DiscardOldestPolicy
, si l'exécuteur n'est pas arrêté, la tâche en tête de la file d'attente de travail est supprimée, puis l'exécution est relancée (ce qui peut échouer à nouveau, provoquant la répétition de cette opération.)
Vous pouvez implémenter une fabrique de threads personnalisée pour les cas d'utilisation ci-dessous:
C'est vrai, ce Executors.newCachedThreadPool()
n'est pas un excellent choix pour le code serveur qui traite plusieurs clients et demandes simultanées.
Pourquoi? Il y a essentiellement deux problèmes (liés) avec cela:
C'est illimité, ce qui signifie que vous ouvrez la porte à quiconque pour paralyser votre JVM en injectant simplement plus de travail dans le service (attaque DoS). Les threads consomment une quantité non négligeable de mémoire et augmentent également la consommation de mémoire en fonction de leur travail en cours, il est donc assez facile de renverser un serveur de cette façon (sauf si vous avez d'autres disjoncteurs en place).
Le problème illimité est exacerbé par le fait que l'exécuteur est dirigé par un, SynchronousQueue
ce qui signifie qu'il y a un transfert direct entre le donneur de tâches et le pool de threads. Chaque nouvelle tâche créera un nouveau thread si tous les threads existants sont occupés. C'est généralement une mauvaise stratégie pour le code serveur. Lorsque le processeur est saturé, les tâches existantes prennent plus de temps à se terminer. Pourtant, plus de tâches sont soumises et plus de threads créés, de sorte que les tâches prennent de plus en plus de temps à terminer. Lorsque le processeur est saturé, plus de threads n'est certainement pas ce dont le serveur a besoin.
Voici mes recommandations:
Utilisez un pool de threads de taille fixe Executors.newFixedThreadPool ou un ThreadPoolExecutor. avec un nombre maximum de threads défini;
La ThreadPoolExecutor
classe est l'implémentation de base des exécuteurs renvoyés par de nombreuses Executors
méthodes de fabrique. Alors abordons les pools de threads fixes et mis en cache du ThreadPoolExecutor
point de vue de.
Le constructeur principal de cette classe ressemble à ceci:
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
Le corePoolSize
détermine la taille minimale du pool de threads cible. L'implémentation conserverait un pool de cette taille même s'il n'y a aucune tâche à exécuter.
Le maximumPoolSize
est le nombre maximum de threads qui peuvent être actifs à la fois.
Une fois que le pool de threads augmente et devient plus grand que le corePoolSize
seuil, l'exécuteur peut mettre fin aux threads inactifs et atteindre à corePoolSize
nouveau. Si allowCoreThreadTimeOut
est vrai, l'exécuteur peut même mettre fin aux threads du pool de base s'ils étaient inactifs au-delà du keepAliveTime
seuil.
Donc, l'essentiel est que si les threads restent inactifs au-delà du keepAliveTime
seuil, ils peuvent se terminer car il n'y a pas de demande pour eux.
Que se passe-t-il lorsqu'une nouvelle tâche arrive et que tous les threads principaux sont occupés? Les nouvelles tâches seront mises en file d'attente dans cette BlockingQueue<Runnable>
instance. Lorsqu'un thread devient libre, l'une de ces tâches en file d'attente peut être traitée.
Il existe différentes implémentations de l' BlockingQueue
interface en Java, nous pouvons donc implémenter différentes approches de mise en file d'attente telles que:
File d'attente limitée : les nouvelles tâches seraient mises en file d'attente dans une file d'attente de tâches limitée.
File d'attente illimitée : les nouvelles tâches seraient mises en file d'attente dans une file d'attente de tâches illimitée. Ainsi, cette file d'attente peut augmenter autant que la taille du tas le permet.
Transfert synchrone : nous pouvons également utiliser SynchronousQueue
pour mettre en file d'attente les nouvelles tâches. Dans ce cas, lors de la mise en file d'attente d'une nouvelle tâche, un autre thread doit déjà attendre cette tâche.
Voici comment ThreadPoolExecutor
exécute une nouvelle tâche:
corePoolSize
threads sont en cours d'exécution, essaie de démarrer un nouveau thread avec la tâche donnée comme premier travail.BlockingQueue#offer
méthode. La offer
méthode ne bloquera pas si la file d'attente est pleine et retourne immédiatement false
.offer
retourne false
), il essaie alors d'ajouter un nouveau thread au pool de threads avec cette tâche comme premier travail.RejectedExecutionHandler
.La principale différence entre les pools de threads fixes et mis en cache se résume à ces trois facteurs:
+ ----------- + ----------- + ------------------- + ----- ---------------------------- + | Type de piscine | Taille du noyau | Taille maximale | Stratégie de file d'attente | + ----------- + ----------- + ------------------- + ----- ---------------------------- + | Fixe | n (fixe) | n (fixe) | Unbounded `LinkedBlockingQueue` | + ----------- + ----------- + ------------------- + ----- ---------------------------- + | En cache | 0 | Integer.MAX_VALUE | `SynchronousQueue` | + ----------- + ----------- + ------------------- + ----- ---------------------------- +
Excutors.newFixedThreadPool(n)
marche:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
Comme vous pouvez le voir:
OutOfMemoryError
.Quand dois-je utiliser l'un ou l'autre? Quelle stratégie est la meilleure en termes d'utilisation des ressources?
Un pool de threads de taille fixe semble être un bon candidat lorsque nous allons limiter le nombre de tâches simultanées à des fins de gestion des ressources .
Par exemple, si nous allons utiliser un exécuteur pour gérer les demandes du serveur Web, un exécuteur fixe peut gérer les rafales de demandes plus raisonnablement.
Pour une gestion encore meilleure des ressources, il est fortement recommandé de créer une configuration personnalisée ThreadPoolExecutor
avec une BlockingQueue<T>
implémentation limitée couplée à une solution raisonnable RejectedExecutionHandler
.
Voici comment ça Executors.newCachedThreadPool()
marche:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
Comme vous pouvez le voir:
Integer.MAX_VALUE
. En pratique, le pool de threads est illimité.SynchronousQueue
échoue toujours quand il n'y a personne à l'autre bout pour l'accepter!Quand dois-je utiliser l'un ou l'autre? Quelle stratégie est la meilleure en termes d'utilisation des ressources?
Utilisez-le lorsque vous avez beaucoup de tâches prévisibles à court terme.
Vous devez utiliser newCachedThreadPool uniquement lorsque vous avez des tâches asynchrones de courte durée comme indiqué dans Javadoc, si vous soumettez des tâches qui prennent plus de temps à traiter, vous finirez par créer trop de threads. Vous pouvez atteindre 100% du processeur si vous soumettez des tâches de longue durée à un rythme plus rapide à newCachedThreadPool ( http://rashcoder.com/be-careful- while-using-executors-newcachedthreadpool/ ).
Je fais quelques tests rapides et j'ai les résultats suivants:
1) si vous utilisez SynchronousQueue:
Une fois que les threads atteignent la taille maximale, tout nouveau travail sera rejeté à l'exception comme ci-dessous.
Exception dans le thread "main" java.util.concurrent.RejectedExecutionException: tâche java.util.concurrent.FutureTask@3fee733d rejetée de java.util.concurrent.ThreadPoolExecutor@5acf9800 [En cours d'exécution, taille du pool = 3, threads actifs = 3, tâches en file d'attente = 0, tâches terminées = 0]
à java.util.concurrent.ThreadPoolExecutor $ AbortPolicy.rejectedExecution (ThreadPoolExecutor.java:2047)
2) si vous utilisez LinkedBlockingQueue:
Les threads n'augmentent jamais de la taille minimale à la taille maximale, ce qui signifie que le pool de threads est de taille fixe comme taille minimale.