Que vous suiviez ou non, les applications Web Java utilisent des pools de threads pour gérer les demandes dans une plus grande ou moindre mesure. Les détails d'implémentation des pools de threads peuvent être ignorés, mais il est nécessaire de comprendre tôt ou tard l'utilisation et le réglage des pools de fil. Cet article introduit principalement l'utilisation du pool de threads Java et comment configurer correctement le pool de threads.
Simple
Commençons par les bases. Peu importe le serveur d'applications ou le framework (tels que Tomcat, Jetty, etc.), ils ont des implémentations de base similaires. La base d'un service Web est une prise, qui est responsable de l'écoute du port, d'attente de la connexion TCP et d'accepter la connexion TCP. Une fois la connexion TCP acceptée, les données peuvent être lues et envoyées à partir de la connexion TCP nouvellement créée.
Afin de comprendre le processus ci-dessus, nous n'utilisons aucun serveur d'applications directement, mais créons un service Web simple à partir de zéro. Ce service est un microcosme de la plupart des serveurs d'applications. Un simple service Web unique à thread ressemble à ceci:
SERVERSOCKET LUVERNER = NOUVEAU SERVERSOCKET (8080); try {while (true) {socket socket = écouteur.accept (); essayez {handlerequest (socket); } catch (ioException e) {e.printStackTrace (); }}} Enfin {auditeur.close ();}Le code ci-dessus crée un socket serveur (Serversocket) , écoute le port 8080, puis les boucles pour vérifier la prise pour voir s'il y a une nouvelle connexion. Une fois qu'une nouvelle connexion est acceptée, la prise sera transmise dans la méthode HandleRequest. Cette méthode analyse le flux de données dans une demande HTTP, répond et écrit les données de réponse. Dans cet exemple simple, la méthode HandleRequest implémente simplement la lecture dans le flux de données et renvoie des données de réponse simples. Dans les implémentations générales, cette méthode sera beaucoup plus complexe, comme la lecture des données d'une base de données, etc.
Réponse de chaîne statique finale = "HTTP / 1.0 200 OK / R / N" + "Content-Type: Text / Plain / R / N" + "/ R / N" + "Hello World / R / N"; public static void handleRequest (socket socket) lève ioException {// lire le flux d'entrée, et renvoie "200 ok" try {buttereDreader dans = new BufferedReader (new inputStreamReader (socket.getInputStream ())); log.info (in.readline ()); OutputStream out = socket.getOutputStream (); out.write (réponse.getBytes (standardCharsets.utf_8)); } enfin {socket.close (); }}Puisqu'il n'y a qu'un seul thread pour traiter la demande, chaque demande doit attendre que la demande précédente soit traitée avant de pouvoir répondre. En supposant qu'un temps de réponse de demande est de 100 millisecondes, le nombre de réponses par seconde (TPS) de ce serveur n'est que de 10.
Multithref
Bien que la méthode HandleRequest puisse bloquer sur IO, le CPU peut toujours gérer plus de demandes. Mais dans un seul cas fileté, cela ne peut pas être fait. Par conséquent, la capacité de traitement parallèle du serveur peut être améliorée en créant des méthodes multiples.
Classe statique publique HandleReQuestrunnable Implements Runnable {socket final socket; handlerequestrunnable (socket socket) {this.socket = socket; } public void run () {try {handleRequest (socket); } catch (ioException e) {e.printStackTrace (); }}} Serversocket écouteur = new serversocket (8080); try {while (true) {socket socket = écouteur.accept (); nouveau thread (nouveau handlerequestrunnable (socket)). start (); }} Enfin {auditeur.close ();}Ici, la méthode Accept () est toujours appelée dans le thread principal, mais une fois la connexion TCP établie, un nouveau thread sera créé pour gérer la nouvelle demande, qui consiste à exécuter la méthode HandleRequest dans le texte précédent dans le nouveau thread.
En créant un nouveau thread, le thread principal peut continuer à accepter de nouvelles connexions TCP, et ces demandes peuvent être traitées en parallèle. Cette méthode est appelée "un thread par demande". Bien sûr, il existe d'autres moyens d'améliorer les performances de traitement, telles que le modèle asynchrone axé sur l'événement utilisé par Nginx et Node.js, mais ils n'utilisent pas de pools de threads et ne sont donc pas couverts par cet article.
Dans chaque demande, une implémentation de thread, la création d'un thread (et de destruction ultérieure) a des frais généraux est très coûteux car le JVM et le système d'exploitation doivent allouer des ressources. En outre, l'implémentation ci-dessus a également un problème, c'est-à-dire que le nombre de threads créés est incontrôlable, ce qui peut entraîner l'épuisement rapidement des ressources système.
Ressources épuisées
Chaque thread nécessite une certaine quantité d'espace mémoire de pile. Dans le plus récent JVM 64 bits, la taille de pile par défaut est de 1024 Ko. Si le serveur reçoit un grand nombre de demandes ou que la méthode HandleRequest s'exécute lentement, le serveur peut s'écraser en raison de la création d'un grand nombre de threads. Par exemple, il existe 1000 demandes parallèles, et les 1000 threads créés doivent utiliser 1 Go de mémoire JVM comme espace de pile de threads. De plus, les objets créés lors de l'exécution du code de chaque thread peuvent également être créés sur le tas. Si cette situation s'aggrave, elle dépassera la mémoire du tas JVM et générera une grande quantité d'opérations de collecte des ordures, ce qui entraînera éventuellement un débordement de mémoire (OutofMemoryErrors).
Ces threads consomment non seulement de la mémoire, ils utilisent également d'autres ressources limitées, telles que des poignées de fichiers, des connexions de base de données, etc. Les threads de création incontrôlables peuvent également provoquer d'autres types d'erreurs et de plantages. Par conséquent, un moyen important d'éviter l'épuisement des ressources consiste à éviter les structures de données incontrôlables.
Soit dit en passant, en raison des problèmes de mémoire causés par la taille de la pile de threads, la taille de la pile peut être ajustée via l'interrupteur -XSS. Après avoir réduit la taille de la pile du fil, la surcharge par fil peut être réduite, mais le débordement de la pile (stackOverflowerRors) peut être augmenté. Pour les applications générales, la valeur par défaut 1024KB est trop riche et il peut être plus approprié de le réduire à 256 Ko ou 512 Ko. La valeur minimale autorisée en Java est de 160 Ko.
Piscine
Pour éviter la création continue de nouveaux threads, vous pouvez limiter la limite supérieure du pool de threads en utilisant un pool de threads simple. La piscine de thread gère tous les threads. Si le nombre de threads n'a pas atteint la limite supérieure, le pool de threads crée des threads à la limite supérieure et réutilise autant que possible les threads libres.
SERVERSOCKET LIVEner = new Serversocket (8080); EMMIRESSERVICE EMECTOROR = EMICA.NEWFIXEDTHREADPOOL (4); try {while (true) {socket socket = auditer.Accept (); Executor.Submit (nouveau handlerequestrunnable (socket)); }} Enfin {auditeur.close ();}Dans cet exemple, au lieu de créer directement le thread, le service d'exécution est utilisé. Il soumet les tâches qui doivent être exécutées (besoin d'implémenter l'interface Runnables) dans le pool de threads et exécute le code à l'aide de threads dans le pool de threads. Dans l'exemple, un pool de threads de taille fixe avec un certain nombre de threads de 4 est utilisé pour traiter toutes les demandes. Cela limite le nombre de threads qui gèrent les demandes et limitent également l'utilisation des ressources.
En plus de créer un pool de threads de taille fixe via la méthode NewFixEdThreadpool, la classe Executors fournit également la méthode NewCachedThreadPool. La réutilisation d'un pool de threads peut toujours conduire à un nombre incontrôlable de threads, mais il utilisera les threads inactifs qui ont été créés avant autant que possible. Habituellement, ce type de pool de thread convient aux tâches courtes qui ne sont pas bloquées par des ressources externes.
File d'attente de travail
Après avoir utilisé un pool de threads de taille fixe, si tous les threads sont occupés, que se passera-t-il si une autre demande arrive? ThreadPoolExecutor utilise une file d'attente pour maintenir les demandes en attente, et les pools de threads de taille fixe utilisent des listes liées illimitées par défaut. Notez que cela peut à son tour causer des problèmes d'épuisement des ressources, mais cela ne se produira pas tant que la vitesse de traitement du thread est supérieure au taux de croissance de la file d'attente. Ensuite, dans l'exemple précédent, chaque demande en file d'attente contiendra une prise, qui dans certains systèmes d'exploitation consommera la poignée de fichier. Étant donné que le système d'exploitation limite le nombre de poignées de fichiers ouvertes par le processus, il est préférable de limiter la taille de la file d'attente de travail.
Public Static ExecutorService NewBoundEdFixEdThreadpool (int nThreads, int la capacité) {return new ThreadPoolExecutor (nthreads, nthreads, 0l, timeunit.milliseconds, New LinkedBlockeue <runnable> (capacité), New ThreadPooLexeToror.DiscardPolicy ()); lance ioException {Serversocket écouteur = new serversocket (8080); ExecutorService exécutor = NewBoundEdFixEdThreadPool (4, 16); essayez {while (true) {socket socket = écouteur.accept (); Executor.Submit (nouveau handlerequestrunnable (socket)); }} enfin {auditeur.close (); }}Ici, au lieu d'utiliser directement la méthode des exécuteurs.
Si tous les fils sont occupés, la nouvelle tâche sera remplie dans la file d'attente. Étant donné que la file d'attente limite la taille à 16 éléments, si cette limite est dépassée, elle doit être gérée par le dernier paramètre lors de la construction de l'objet ThreadPoolExecutor. Dans l'exemple, une discardpolicy est utilisée, c'est-à-dire que lorsque la file d'attente atteint la limite supérieure, la nouvelle tâche sera rejetée. En plus de la première fois, il existe également une politique d'abandon (abortpolicy) et une politique d'exécution de l'appelant (CalleRrunspolicy). Le premier lancera une exception, tandis que le second exécutera la tâche dans le fil de l'appelant.
Pour les applications Web, la stratégie par défaut optimale devrait être d'abandonner ou d'abandonner la stratégie et de renvoyer une erreur au client (comme une erreur HTTP 503). Bien sûr, il est également possible d'éviter d'abandonner les demandes des clients en augmentant la durée de la file d'attente de travail, mais les demandes des utilisateurs ne sont généralement pas disposées à attendre longtemps, ce qui consommera plus de ressources de serveur. Le but de la file d'attente de travail n'est pas de répondre aux demandes des clients sans limite, mais de lisser et d'éclater les demandes. Normalement, la file d'attente de travail doit être vide.
Digne de comptage de threads
L'exemple précédent montre comment créer et utiliser un pool de threads, mais le problème de base avec l'utilisation d'un pool de threads est le nombre de threads qui doivent être utilisés. Tout d'abord, nous devons nous assurer que lorsque la limite de thread est atteinte, la ressource ne sera pas épuisée. Les ressources ici incluent la mémoire (tas et la pile), le nombre de poignées de fichiers ouvertes, le nombre de connexions TCP, le nombre de connexions de base de données distantes et d'autres ressources limitées. En particulier, si les tâches filetées sont à forte intensité de calcul, le nombre de cœurs CPU est également l'une des limitations des ressources. D'une manière générale, le nombre de threads ne doit pas dépasser le nombre de noyaux de processeur.
Étant donné que la sélection du nombre de threads dépend du type d'application, cela peut prendre beaucoup de tests de performances avant que les résultats optimaux puissent être obtenus. Bien sûr, vous pouvez également améliorer les performances de votre application en augmentant le nombre de ressources. Par exemple, modifiez la taille de la mémoire du tas JVM ou modifiez la limite supérieure de la poignée du fichier du système d'exploitation, etc. Ensuite, ces ajustements finiront par toucher la limite supérieure théorique.
La loi de Little
La loi de Little décrit la relation entre trois variables dans un système stable.
Lorsque L représente le nombre moyen de demandes, λ représente la fréquence des demandes et W représente le temps moyen pour répondre à la demande. Par exemple, si le nombre de demandes par seconde est de 10 et que chaque temps de traitement des demandes est de 1 seconde, alors à tout moment, il y a 10 demandes en cours de traitement. Retour à notre sujet, il nécessite 10 threads pour traiter. Si le temps de traitement d'une seule demande double, le nombre de threads traités doublera également, devenant 20.
Après avoir compris l'impact du temps de traitement sur l'efficacité du traitement des demandes, nous constatons que la limite supérieure théorique peut ne pas être la valeur optimale pour la taille du pool de threads. La limite supérieure du pool de filetage nécessite également un temps de traitement des tâches de référence.
En supposant que le JVM peut traiter 1000 tâches en parallèle, si chaque temps de traitement de demande ne dépasse pas 30 secondes, alors dans le pire des cas, au plus 33,3 demandes par seconde peuvent être traitées. Cependant, si chaque demande ne prend que 500 millisecondes, la demande peut traiter 2000 demandes par seconde.
Piscine divisée
Dans les microservices ou les architectures axées sur les services (SOA), l'accès à plusieurs services backend est généralement requis. Si l'un des services fonctionne dégradés, il peut entraîner la manège du pool de threads à court de threads, ce qui affecte les demandes à d'autres services.
Un moyen efficace de gérer la défaillance du service backend consiste à isoler le pool de threads utilisé par chaque service. Dans ce mode, il existe toujours un pool de threads réparti qui envoie des tâches à différents pools de threads de demande de backend. Ce pool de threads peut ne pas avoir de charge en raison d'un backend lent et transférer le fardeau vers un pool de threads qui demande le backend lent.
De plus, le mode de mise en commun multi-thread doit également éviter les problèmes de blocage. Si chaque thread bloque en attendant le résultat d'une demande non transformée, une impasse se produit. Par conséquent, en mode pool multithread, il est nécessaire de comprendre les tâches exécutées par chaque pool de threads et les dépendances entre eux, afin d'éviter autant que possible les problèmes de blocage.
Résumer
Même si les pools de thread ne sont pas utilisés directement dans l'application, ils sont susceptibles d'être utilisés indirectement par le serveur d'applications ou le cadre de l'application. Des cadres tels que Tomcat, JBoss, Undertow, Dropwizard, etc. offrent tous des options pour régler les pools de threads (pools de threads utilisés par l'exécution du servlet).
J'espère que cet article pourra améliorer votre compréhension du pool de fil et vous aider à apprendre.