1. Pourquoi utiliser des pools de threads
De nombreuses applications serveur telles que les serveurs Web, les serveurs de bases de données, les serveurs de fichiers ou les serveurs de messagerie sont orientées vers le traitement d'un grand nombre de tâches courtes provenant d'une source distante. La requête arrive au serveur d'une manière ou d'une autre, peut-être via un protocole réseau (tel que HTTP, FTP ou POP), via une file d'attente JMS ou peut-être en interrogeant une base de données. Quelle que soit la manière dont les requêtes arrivent, une situation courante dans les applications serveur est que le temps de traitement d'une seule tâche est très court mais que le nombre de requêtes est énorme.
Un modèle simple pour créer des applications serveur consiste à créer un nouveau thread à chaque fois qu'une requête arrive, puis à transmettre la requête dans le nouveau thread. Cette approche fonctionne bien pour le prototypage, mais si vous essayez de déployer une application serveur qui s'exécute de cette manière, les graves défauts de cette approche deviennent évidents. L'un des inconvénients de l'approche thread par requête est que la création d'un nouveau thread pour chaque requête coûte cher ; le serveur qui crée un nouveau thread pour chaque requête passe beaucoup de temps à créer et à détruire des threads. Cela consomme plus de temps et de système. ressources que celles dépensées pour traiter les demandes réelles des utilisateurs.
En plus de la surcharge liée à la création et à la destruction des threads, les threads actifs consomment également des ressources système. La création d'un trop grand nombre de threads dans une JVM peut entraîner un manque de mémoire du système ou un « overswitch » en raison d'une consommation excessive de mémoire. Pour éviter la pénurie de ressources, les applications serveur ont besoin d'un moyen de limiter le nombre de requêtes qu'elles peuvent traiter à un moment donné.
Les pools de threads fournissent des solutions aux problèmes de surcharge du cycle de vie des threads et aux problèmes de pénurie de ressources. En réutilisant les threads pour plusieurs tâches, la surcharge de création de threads est répartie sur plusieurs tâches. L'avantage est que, comme le thread existe déjà lorsque la requête arrive, le retard provoqué par la création du thread est également éliminé par inadvertance. De cette façon, les demandes peuvent être traitées immédiatement, ce qui rend l'application plus réactive. De plus, en ajustant de manière appropriée le nombre de threads dans le pool de threads, c'est-à-dire lorsque le nombre de requêtes dépasse un certain seuil, toutes les autres requêtes entrantes sont obligées d'attendre qu'un thread soit obtenu pour les traiter, évitant ainsi les pénuries de ressources.
2. Risques liés à l'utilisation de pools de threads
Bien que les pools de threads soient un mécanisme puissant pour créer des applications multithread, leur utilisation n'est pas sans risques. Les applications créées avec des pools de threads sont sensibles à tous les risques de concurrence auxquels toute autre application multithread est susceptible, tels que les erreurs de synchronisation et les blocages. Elles sont également sensibles à quelques autres risques spécifiques aux pools de threads, tels que les blocages liés aux pools. , Ressources insuffisantes et fuites de threads.
2.1 Deadlock
Toute application multithread présente un risque de blocage. Lorsque chacun d'un groupe de processus ou de threads attend un événement qui ne peut être provoqué que par un autre processus du groupe, on dit que le groupe de processus ou de threads est dans une impasse. Le cas le plus simple de blocage est le suivant : le thread A détient un verrou exclusif sur l'objet X et attend un verrou sur l'objet Y, tandis que le thread B détient un verrou exclusif sur l'objet Y mais attend un verrou sur l'objet X. À moins qu'il existe un moyen d'interrompre l'attente du verrou (le verrouillage Java ne prend pas en charge cette méthode), le thread bloqué attendra indéfiniment.
Bien qu'il existe un risque de blocage dans tout programme multithread, les pools de threads introduisent une autre possibilité de blocage, dans laquelle tous les threads du pool s'exécutent dans une file d'attente bloquée. Une tâche qui est le résultat de l'exécution de une autre tâche, mais cette tâche ne peut pas s'exécuter car il n'y a pas de threads inoccupés. Cela se produit lorsqu'un pool de threads est utilisé pour implémenter une simulation impliquant de nombreux objets en interaction, les objets simulés peuvent s'envoyer des requêtes les uns aux autres, et ces requêtes sont ensuite exécutées en tant que tâches en file d'attente, tandis que les objets de requête attendent des réponses de manière synchrone.
2.2 Ressources insuffisantes
L'un des avantages des pools de threads est qu'ils fonctionnent généralement très bien par rapport à d'autres mécanismes de planification alternatifs (dont certains ont déjà été évoqués). Mais cela n’est vrai que si la taille du pool de threads est ajustée de manière appropriée. Les threads consomment beaucoup de ressources, notamment de la mémoire et d'autres ressources système. En plus de la mémoire requise par l'objet Thread, chaque thread nécessite deux piles d'appels d'exécution, qui peuvent être volumineuses. De plus, la JVM peut créer un thread natif pour chaque thread Java, et ces threads natifs consommeront des ressources système supplémentaires. Enfin, bien que la surcharge de planification liée au basculement entre les threads soit faible, s'il y a de nombreux threads, le changement de contexte peut sérieusement affecter les performances du programme.
Si le pool de threads est trop volumineux, les ressources consommées par ces threads peuvent sérieusement affecter les performances du système. Basculer entre les threads vous fera perdre du temps et utiliser plus de threads que ce dont vous avez réellement besoin peut entraîner des problèmes de manque de ressources, car les threads du pool consomment des ressources qui peuvent être utilisées plus efficacement par d'autres tâches. En plus des ressources utilisées par le thread lui-même, le travail effectué pour traiter la requête peut nécessiter d'autres ressources, telles que des connexions JDBC, des sockets ou des fichiers. Ce sont également des ressources limitées, et un trop grand nombre de requêtes simultanées peuvent provoquer des échecs, comme l'impossibilité d'allouer une connexion JDBC.
2.3 Erreur de concurrence
Les pools de threads et autres mécanismes de mise en file d'attente reposent sur l'utilisation des méthodes wait() et notify(), qui sont toutes deux difficiles à utiliser. Si elles ne sont pas codées correctement, les notifications peuvent être perdues, ce qui fait que le thread reste inactif même s'il y a du travail à traiter dans la file d'attente. Une extrême prudence doit être prise lors de l’utilisation de ces méthodes. Au lieu de cela, il est préférable d’utiliser une implémentation existante dont on sait déjà qu’elle fonctionne, telle que le package util.concurrent.
2.4 Fuite de threads
Un risque sérieux dans divers types de pools de threads est la fuite de threads, lorsqu'un thread est retiré du pool pour effectuer une tâche et qu'une fois la tâche terminée, le thread Cela se produit lorsque la piscine n'est pas restituée. Une situation dans laquelle des fuites de thread se produisent est lorsqu'une tâche renvoie une RuntimeException ou une erreur. Si la classe pool ne les intercepte pas, le thread se fermera simplement et la taille du pool de threads sera définitivement réduite de un. Lorsque cela se produit suffisamment de fois, le pool de threads finit par se vider et le système se bloque car aucun thread n'est disponible pour gérer la tâche.
Certaines tâches peuvent attendre indéfiniment certaines ressources ou entrées de l'utilisateur, et il n'est pas garanti que ces ressources deviennent disponibles, l'utilisateur peut être rentré chez lui, et ces tâches seront définitivement arrêtées, et ces tâches arrêtées peuvent provoquent également les mêmes problèmes que les fuites de threads. Si un thread est consommé de manière permanente par une telle tâche, il est effectivement supprimé du pool. Pour de telles tâches, vous devez soit leur donner uniquement leur propre fil de discussion, soit les laisser attendre pendant un temps limité.
2.5 Surcharge de requêtes
Il est possible de submerger le serveur avec juste des requêtes. Dans ce scénario, nous ne souhaitons peut-être pas mettre en file d'attente chaque requête entrante dans notre file d'attente de travail, car les tâches mises en file d'attente pour exécution peuvent consommer trop de ressources système et entraîner une pénurie de ressources. Ce que vous décidez de faire dans cette situation dépend de vous ; dans certains cas, vous pouvez simplement abandonner la demande et compter sur un protocole de niveau supérieur pour réessayer la demande plus tard, ou vous pouvez répondre avec une réponse indiquant que le serveur est temporairement occupé. pour refuser la demande.
3. Directives pour utiliser efficacement les pools de threads
Les pools de threads peuvent être un moyen extrêmement efficace de créer des applications serveur à condition de suivre quelques directives simples :
Ne le faites pas. t Mettre en file d'attente les tâches qui attendent de manière synchrone les résultats d'autres tâches. Cela peut conduire à la forme de blocage décrite ci-dessus, dans laquelle tous les threads sont occupés par des tâches qui attendent à leur tour les résultats des tâches en file d'attente qui ne peuvent pas être exécutées car tous les threads sont très occupés.
Soyez prudent lorsque vous utilisez des threads regroupés pour des opérations potentiellement longues. Si le programme doit attendre la fin d'une ressource telle qu'une E/S, spécifiez le temps d'attente maximum et s'il faut ensuite invalider ou remettre la tâche en file d'attente pour une exécution ultérieure. Cela garantit que certains progrès seront éventuellement réalisés en libérant un fil de discussion pour une tâche susceptible de se terminer avec succès.
Comprenez la tâche. Pour dimensionner efficacement le pool de threads, vous devez comprendre les tâches qui sont mises en file d'attente et ce qu'elles font. Sont-ils liés au processeur ? Sont-ils liés aux E/S ? Votre réponse affectera la façon dont vous ajusterez votre candidature. Si vous disposez de différentes classes de tâches avec des caractéristiques très différentes, il peut être judicieux d'avoir plusieurs files d'attente de travail pour différentes classes de tâches afin que chaque pool puisse être ajusté en conséquence.
4. Paramètre de la taille du pool de threads
Ajuster la taille du pool de threads consiste essentiellement à éviter deux types d'erreurs : trop peu de threads ou trop de threads. Heureusement, pour la plupart des applications, la marge entre trop et pas assez est assez large.
Rappel : Il y a deux avantages principaux à utiliser des threads dans une application, permettre au traitement de continuer malgré l'attente d'opérations lentes telles que les E/S, et tirer parti de plusieurs processeurs. Dans les applications exécutées sous les contraintes de calcul d'une machine dotée de N processeurs, l'ajout de threads supplémentaires lorsque le nombre de threads approche N peut améliorer la puissance de traitement globale, tandis que l'ajout de threads supplémentaires lorsque le nombre de threads dépasse N n'aura aucun effet. En fait, un trop grand nombre de threads peut même dégrader les performances, car cela entraîne une surcharge supplémentaire en matière de changement de contexte.
La taille optimale du pool de threads dépend du nombre de processeurs disponibles et de la nature des tâches dans la file d'attente de travail. S'il n'y a qu'une seule file d'attente de travail sur un système doté de N processeurs, qui sont tous des tâches de calcul, l'utilisation maximale du processeur sera généralement obtenue lorsque le pool de threads comporte N ou N 1 threads.
Pour les tâches qui peuvent devoir attendre la fin des E/S (par exemple, les tâches qui lisent les requêtes HTTP à partir d'un socket), vous devez laisser la taille du pool dépasser le nombre de processeurs disponibles, car tous ne les threads fonctionneront toujours. En utilisant le profilage, vous pouvez estimer le rapport entre le temps d'attente (WT) et le temps de service (ST) pour une demande typique. Si nous appelons ce rapport WT/ST, alors pour un système avec N processeurs, environ N*(1 WT/ST) threads devraient être configurés pour que les processeurs restent pleinement utilisés.
L'utilisation du processeur n'est pas la seule considération lors du dimensionnement du pool de threads. À mesure que votre pool de threads augmente, vous pouvez rencontrer des limites sur le planificateur, la mémoire disponible ou d'autres ressources système, telles que le nombre de sockets, de descripteurs de fichiers ouverts ou de connexions à la base de données.
5. Plusieurs pools de threads couramment utilisés
5.1 newCachedThreadPool
Créez un pool de threads pouvant être mis en cache si la longueur du pool de threads dépasse les besoins de traitement, les threads inactifs peuvent être recyclés de manière flexible. . S'il n'y a pas de recyclage, créez un nouveau fil de discussion.
Les caractéristiques de ce type de pool de threads sont :
• Il n'y a presque aucune limite sur le nombre de threads de travail créés (en fait, il y a une limite, le nombre est entier. MAX_VALUE ), afin que le pool de Threads puisse être ajouté de manière flexible Ajouter un fil dans .
• Si aucune tâche n'est soumise au pool de threads pendant une longue période, c'est-à-dire si le thread de travail est inactif pendant la durée spécifiée (la valeur par défaut est 1 minute), le thread de travail se terminera automatiquement. Après la résiliation, si vous soumettez une nouvelle tâche, le pool de threads recréera un thread de travail.
• Lorsque vous utilisez CachedThreadPool, vous devez faire attention au contrôle du nombre de tâches, sinon, en raison d'un grand nombre de threads exécutés en même temps, le système pourrait être paralysé.
L'exemple de code est le suivant :
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService cachedThreadPool = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int index = i; try { Thread.sleep(index * 1000); } catch (InterruptedException e) { e.printStackTrace(); } cachedThreadPool.execute(new Runnable() { public void run() { System.out.println(index); } }); } } }
5.1 newFixedThreadPool
Créer un pool de threads avec un nombre spécifié de threads de travail. Chaque fois qu'une tâche est soumise, un thread de travail est créé. Si le nombre de threads de travail atteint le nombre maximum initial du pool de threads, la tâche soumise est stockée dans la file d'attente du pool.
FixedThreadPool est un pool de threads typique et excellent. Il présente les avantages d'un pool de threads, améliorant l'efficacité du programme et économisant les frais généraux lors de la création de threads. Cependant, lorsque le pool de threads est inactif, c'est-à-dire lorsqu'il n'y a aucune tâche exécutable dans le pool de threads, il ne libérera pas les threads de travail et occupera également certaines ressources système.
L'exemple de code est le suivant :
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int index = i; fixedThreadPool.execute(new Runnable() { public void run() { try { System.out.println(index); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
Étant donné que la taille du pool de threads est de 3, chaque tâche est en veille pendant 2 secondes après la sortie. l'index, donc chaque tâche imprime 3 nombres en deux secondes.
Il est préférable de définir la taille du pool de threads de longueur fixe en fonction des ressources système telles que Runtime.getRuntime().availableProcessors().
5.1 newSingleThreadExecutor
Créez un exécuteur à thread unique, c'est-à-dire créez uniquement un thread de travail unique pour exécuter les tâches. Il utilisera uniquement le seul thread de travail pour exécuter les tâches, garantissant ainsi que toutes les tâches. suivre l'exécution dans l'ordre spécifié (FIFO, LIFO, priorité). Si ce thread se termine anormalement, un autre le remplacera pour assurer une exécution séquentielle. La plus grande caractéristique d'un seul thread de travail est qu'il peut garantir que les tâches sont exécutées de manière séquentielle et qu'aucun thread multiple n'est actif à un moment donné.
L'exemple de code est le suivant :
package test; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolExecutorTest { public static void main(String[] args) { ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; singleThreadExecutor.execute(new Runnable() { public void run() { try { System.out.println(index); Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } } }); } } }
5.1 newScheduleThreadPool
Crée un pool de threads de longueur fixe et prend en charge la synchronisation et l'exécution des tâches périodiques, prenant en charge l'exécution des tâches planifiées et périodiques.
Retarde l'exécution de 3 secondes. L'exemple de code pour une exécution retardée est le suivant :
package test; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorTest { public static void main(String[] args) { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); scheduledThreadPool.schedule(new Runnable() { public void run() { System.out.println("delay 3 seconds"); } }, 3, TimeUnit.SECONDS); } }
signifie qu'il sera exécuté toutes les 3 secondes après un délai de 1. Deuxièmement. L'exemple de code pour une exécution régulière est le suivant :
package test; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; public class ThreadPoolExecutorTest { public static void main(String[] args) { ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5); scheduledThreadPool.scheduleAtFixedRate(new Runnable() { public void run() { System.out.println("delay 1 seconds, and excute every 3 seconds"); } }, 1, 3, TimeUnit.SECONDS); } }
L'article ci-dessus discute brièvement de la comparaison de plusieurs pools de threads couramment utilisés en Java. Il s'agit de tout le contenu partagé par. l'éditeur. J'espère qu'il pourra vous donner une référence et j'espère que vous le soutiendrez sur le site Web PHP chinois.
Pour plus d'articles sur la comparaison de plusieurs pools de threads couramment utilisés en Java, veuillez faire attention au site Web PHP chinois !