Contexte technique du pool de threads
Dans la programmation orientée objet, la création et la destruction d'objets prennent du temps, car la création d'un objet nécessite des ressources de mémoire ou d'autres ressources supplémentaires. C'est encore plus dans Java, où la machine virtuelle essaiera de suivre chaque objet afin qu'il puisse être collecté après la détruire de l'objet.
Par conséquent, une façon d'améliorer l'efficacité des programmes de service est de minimiser le nombre de fois de création et de destruction d'objets, en particulier certaines créations et destructions d'objets très consommateurs de ressources. Comment utiliser les objets existants pour servir est un problème clé qui doit être résolu. En fait, c'est la raison pour laquelle certaines technologies de «ressources de mise en commun» sont produites.
Par exemple, de nombreux composants communs couramment observés dans Android sont généralement inséparables du concept de «pool», tels que diverses bibliothèques de chargement d'images et des bibliothèques de demandes de réseau. Même si Meaasge dans le mécanisme de messagerie d'Android utilise meaasge.obtain (), c'est l'objet dans la piscine Meaasge utilisée, donc ce concept est très important. La technologie de mise en commun des fils introduite dans cet article est également conforme à cette idée.
Avantages du pool de threads:
1. Réutiliser les fils dans le pool de threads pour réduire les frais généraux de performances causés par la création et la destruction d'objets;
2. Il peut contrôler efficacement le nombre maximal de threads de concurrence, améliorer l'utilisation des ressources du système et éviter une concurrence excessive des ressources et éviter le blocage;
3. Capacité à effectuer une gestion simple de plusieurs threads, ce qui rend l'utilisation de threads simples et efficaces.
Exécuteur exécuteur du cadre de pool de threads
Le pool de threads de Java est implémenté via le cadre de l'exécuteur. Le cadre exécuteur comprend des classes: exécuteur testamentaire, exécuteur, exécuteur EXIMPORTORS, ThreadPoolExecutor, Callable et Future, FutureTask Utilisation, etc.
Exécuteur: il n'y a qu'une seule méthode pour toutes les interfaces de pool de threads.
Interface publique Executor {void execute (Commande Runnable); }ExecutorService: l'ajout du comportement de l'exécuteur est l'interface la plus directe de la classe d'implémentation d'exécuteur.
Exécuteurs: fournit une série de méthodes d'usine pour créer le pool de threads, et les pools de threads renvoyés implémentent tous l'interface ExecutorService.
ThreadPoolExecutor: la classe d'implémentation spécifique des pools de threads. Les différents pools de threads généralement utilisés sont implémentés en fonction de cette classe. La méthode de construction est la suivante:
Public ThreadPoolExecutor (int corepoolSize, int maximumpoolSize, long keepalivetime, timeunit Unit, BlockerQueue <cunnable> workQueue) {this (CorepoolSize, maximumpoolSize, keepalivetime, unit, workqueue, exécutors.defaultthreadfactory (), defaulthandler);}CorePoolSize: le nombre de threads de base dans le pool de threads et le nombre de threads exécutés dans le pool de threads ne dépasseront jamais CorePoolSize et pourra survivre par défaut. Vous pouvez définir AllowCoreThreadTimeout sur true, le nombre de threads de base est de 0 et KeepaliveTime contrôle le temps d'attente de tous les threads.
MaximumpoolSize: le nombre maximal de threads autorisés par le pool de threads;
KeepaliveTime: fait référence à l'heure du délai d'expiration où se termine le fil inactif;
Unité: est une énumération, représentant l'unité de KeepaliveTime;
WorkQueue: représente la file d'attente BlockingQueue <Runnable qui stocke la tâche.
BlockingQueue: BlockingQueue est un outil principalement utilisé pour contrôler la synchronisation du thread sous java.util.concurrent. Si la blockqueue est vide, le fonctionnement de la récupération de quelque chose à partir du blocage est bloqué et ne sera pas éveillé jusqu'à ce que le BlocingQueue entre la chose. De même, si le BlockingQueue est plein, toute opération qui tente d'y stocker les choses sera bloquée et ne sera pas éveillée pour poursuivre ses opérations jusqu'à ce qu'il y ait de l'espace dans le blocage. Les files d'attente de blocage sont souvent utilisées dans des scénarios de producteurs et de consommateurs. Les producteurs sont des threads qui ajoutent des éléments aux files d'attente, et les consommateurs sont des threads qui prennent des éléments des files d'attente. Une file d'attente de blocage est le conteneur où le producteur stocke les éléments, et le consommateur ne prend que des éléments du conteneur. Les classes de mise en œuvre spécifiques incluent LinkedBlockingQueue, ArrayBlockingQueued, etc. Généralement, le blocage interne et le réveil sont obtenus par verrouillage et condition (apprentissage et utilisation des verrous et conditions d'affichage).
Le processus de travail du pool de threads est le suivant:
Lorsque le pool de fils a été créé pour la première fois, il n'y avait pas de fil à l'intérieur. La file d'attente de tâches est transmise en tant que paramètre. Cependant, même s'il y a des tâches dans la file d'attente, le pool de threads ne les exécutera pas immédiatement.
Lors de l'ajout d'une tâche en appelant la méthode Execure (), le pool de threads fera le jugement suivant:
Si le nombre de threads en cours d'exécution est inférieur à CorePoolSize, créez un thread pour exécuter cette tâche immédiatement;
Si le nombre de threads en cours d'exécution est supérieur ou égal à CorePoolSize, mettez cette tâche dans la file d'attente;
Si la file d'attente est pleine pour le moment et que le nombre de threads en cours d'exécution est inférieur à MaximumpoolSize, vous devez toujours créer un thread non fondamental pour exécuter la tâche immédiatement;
Si la file d'attente est pleine et que le nombre de threads en cours d'exécution est supérieur ou égal à MaximumpoolSize, le pool de threads lancera une exception RejectExecutionException.
Lorsqu'un thread termine une tâche, il faut la tâche suivante de la file d'attente à exécuter.
Lorsqu'un fil n'a rien à faire, et après une certaine période de temps (keepalivetime), le pool de thread jugera que si le nombre de threads en cours d'exécution est supérieur à CorePoolSize, le fil sera arrêté. Ainsi, une fois toutes les tâches du pool de fils terminées, il finira par rétrécir la taille de CorePoolSize.
Création et utilisation de pools de fil
Le pool de threads de génération utilise la méthode statique des exécuteurs de la classe d'outils. Voici plusieurs pools de fil condurs.
SingleThreAdExecutor: Thread de fond unique (la file d'attente de tampon est illimitée)
public static ExecutorService NewsingLetHreAdExecutor () {return new FinaliseableDelegatedExecutService (new ThreadPoolExecutor (1, 1, 0l, timeunit.milliseconds, new LinkedBlockeue <Nuenable> ())); }Créez un seul pool de threads. Ce pool de threads n'a qu'un seul fil de cœur fonctionnant, ce qui équivaut à un seul thread effectuant toutes les tâches en série. Si ce fil unique se termine en raison de l'exception, il y aura un nouveau fil pour le remplacer. Ce pool de threads garantit que l'ordre d'exécution de toutes les tâches est exécuté dans l'ordre de la soumission de tâche.
FixedThreadpool: Seulement le pool de threads de threads de base, avec une taille fixe (la file d'attente tampon est illimitée).
public static ExecutorService newFixEdThreadpool (int nThreads) {
Renvoie un nouveau threadpoolExecutor (nthreads, nthreads,
0l, timeUnit.Millisecondes,
Nouveau LinkedBlockingQueue <Runnable> ());
}
Créez un pool de threads de taille fixe. Chaque fois qu'une tâche est soumise, un thread est créé jusqu'à ce que le thread atteigne la taille maximale du pool de threads. La taille du pool de threads reste la même une fois qu'elle atteint sa valeur maximale. Si un thread se termine en raison d'une exception d'exécution, le pool de threads ajoutera un nouveau thread.
CachedThreadpool: Pool de fil illimité peut effectuer un recyclage automatique des filetages.
public static ExecutorService newcachedThreadpool () {return new ThreadPoolExecutor (0, Integer.max_value, 60L, timeunit.seconds, new SynchronousQueue <Runnable> ()); }Si la taille du pool de thread dépasse le thread requis pour traiter la tâche, certains des threads inactifs (aucune exécution de tâche en 60 secondes) seront recyclés. Lorsque le nombre de tâches augmente, ce pool de threads peut ajouter intelligemment de nouveaux threads pour traiter la tâche. Ce pool de thread ne limite pas la taille du pool de threads, ce qui dépend entièrement de la taille maximale du thread que le système de fonctionnement (ou JVM) peut créer. Synchronousqueue est une file d'attente de blocage avec du tampon de 1.
ScheduledThreadpool: un pool de threads de base avec pool de threads à noyau fixe, taille illimitée. Ce pool de fils prend en charge la nécessité d'effectuer des tâches périodiquement et périodiquement.
public static ExecutorService NewsCheDuleDThreadpool (int corepoolSize) {return new scheduledThreadpool (corepoolSize, Integer.max_value, default_keepalive_milis, milliseconds, new DelayedWorkQueue ()); }Créez un pool de fils qui effectue des tâches périodiquement. Si le ralenti, le pool de threads non core sera recyclé dans le temps default_keepalivemillis.
Il existe deux méthodes les plus couramment utilisées pour soumettre des tâches dans les pools de threads:
exécuter:
ExecutorService.Exécute (RANNABLE RANable);
Soumettre:
FutureTask Task = ExecutorService.Submit (Runnable Runnable);
FutureTask <T> Task = ExecutorService.Submit (Runnable Runnable, T Results);
FutureTask <T> Task = ExecutorService.Submit (callable <t> callable);
Il en va de même pour la mise en œuvre de SOUMOR (CALLABLE CALLable) et il en va de même pour soumettre (Runnable Runnable).
public <T> futur <T> soumettre (tâche callable <t>) {if (task == null) lance un nouveau nullpointerException (); FutureTask <T> ftask = newtaskfor (tâche); exécuter (ftask); retour ftask;}On peut voir que la soumission est une tâche qui renvoie un résultat et renverra un objet FutureTask, afin que le résultat puisse être obtenu via la méthode get (). L'appel final à soumettre est également exécuté (Runnable Runable). Soumettre résume uniquement l'objet appelant ou exécutable dans un objet FutureTask. Étant donné que FutureTask est un runnable, il peut être exécuté dans EXECUTE. Pour la façon dont les objets appelés et Runnable sont encapsulés dans des objets FutureTask, consultez l'utilisation de FutureTask, futur, futur.
Le principe de la mise en œuvre du pool de threads
Si vous ne parlez que de l'utilisation de pools de fil, ce blog n'a pas de grande valeur. Au mieux, ce n'est qu'un processus de se familiariser avec l'API lié à l'exécuteur testamentaire. Le processus d'implémentation du pool de threads n'utilise pas le mot clé synchronisé, mais utilise des files d'attente volatiles, verrouillées et synchrones (blocage), classes liées à l'atomique, Futuretask, etc., car ce dernier a de meilleures performances. Le processus de compréhension peut bien apprendre l'idée d'un contrôle simultané dans le code source.
Les avantages de la mise en commun des threads mentionnés au début sont résumés comme suit:
Réutilisation du fil
Contrôler le nombre maximum de concurrences
Gérer les fils
1. Processus de multiplexage du fil
Pour comprendre le principe du multiplexage du fil, vous devez d'abord comprendre le cycle de vie du fil.
Pendant le cycle de vie d'un fil, il doit passer par cinq États: nouveau, coulant, en cours d'exécution, bloqué et mort.
Le thread crée un nouveau fil à travers de nouveaux. Ce processus consiste à initialiser certaines informations sur le thread, telles que le nom du thread, l'identifiant, le groupe auquel appartient le thread, etc., qui peut être considéré comme un objet ordinaire. Après Start () de Thread d'appel, la machine virtuelle Java crée une pile d'appels de méthode et un compteur de programme pour cela, et en même temps, HasbeEenStarted to true, puis il y aura une exception lors de l'appel de la méthode de démarrage.
Le thread de cet état ne commence pas à fonctionner, mais signifie seulement que le thread peut fonctionner. Quant au moment où le thread commence à fonctionner, cela dépend du planificateur dans le JVM. Lorsque le thread obtient le CPU, la méthode run () sera appelée. N'appelez pas vous-même la méthode Run () de Thread. Ensuite, bascule entre le blocage prêt à l'emploi en fonction de la planification du CPU, jusqu'à ce que la méthode Run () se termine ou d'autres méthodes arrêtent le thread et entrez l'état mort.
Par conséquent, le principe d'implémentation de réutilisation du thread devrait être de maintenir le fil en vie (prêt, en cours d'exécution ou bloquant). Ensuite, jetons un coup d'œil à la façon dont ThreadPoolExecutor implémente la réutilisation du thread.
La classe de travailleurs principale dans ThreadPoolExecutor contrôle la réutilisation du thread. Jetez un œil au code simplifié de la classe de travailleurs, il est donc facile à comprendre:
La classe finale privée implémente Runnable {Final Thread Thread; Runnable FirstTask; Worker (Runnable FirstTask) {this.FirstTask = FirstTask; this.Thread = GetThreadFactory (). NewThread (this);} public void run () {runworker (this);} final Void Runworker = Worker W) {runnable tâche null; while (tâche! = null || (task = getTask ())! = null) {task.run ();}}Le travailleur est un coulable et a un fil en même temps. Ce fil est le fil à ouvrir. Lors de la création d'un nouvel objet de travail, un nouvel objet de thread est créé en même temps et le travailleur lui-même est transmis dans TTHREAD en tant que paramètre. De cette façon, lorsque la méthode de thread start () est appelée, la méthode RUN () du travailleur est réellement en cours d'exécution. Ensuite, dans Runworker (), il y a une boucle de temps, qui continue d'obtenir l'objet Runnable de getTask () et l'exécute en séquence. Comment getTask () obtient-il l'objet Runnable?
Toujours le code simplifié:
private runnable getTask () {if (quelques cas spéciaux) {return null; } Runnable r = workqueue.take (); retour r;}Ce WorkQueue est la file d'attente BlockingQueue qui stocke les tâches lors de l'initialisation de ThreadpoolExecutor. La file d'attente stocke les tâches exécutables à exécuter. Parce que BlockingQueue est une file d'attente de blocage, BlockingQueue.Take () devient vide et entre dans l'état d'attente jusqu'à ce qu'un nouvel objet dans BlockingQueue soit ajouté pour réveiller le fil bloqué. Par conséquent, en général, la méthode RUN () du thread ne se terminera pas, mais continuera d'exécuter des tâches Runnables de WorkQueue, qui atteint le principe de la réutilisation du thread.
2. Contrôlez le nombre maximum de concurrences
Alors, quand est runnable mis dans workqueue? Quand le travailleur est-il créé? Quand le thread dans le travailleur appelé start () est-il pour ouvrir un nouveau thread pour exécuter la méthode de travailleur RUN ()? L'analyse ci-dessus montre que le runworker () dans le travailleur effectue des tâches les unes après les autres, alors comment la concurrence se manifeste-t-elle?
Il est facile de penser que vous effectuez certaines des tâches ci-dessus lorsque vous exécutez (Runnable Runnable). Voyons comment cela se fait dans Execute.
exécuter:
Code simplifié
public void execute (Runnable Command) {if (command == null) lance un nouveau nullpointerException (); int c = ctl.get (); // Nombre actuel de threads <corePoolSizeIF (workerCountOf (c) <corePoolSize) {// Démarrez directement un nouveau thread. if (addWorker (Command, true)) return; c = ctl.get ();} // nombre de threads actifs> = corePoolSize // runState est en cours d'exécution et que la file d'attente n'est pas complète if (isrunning (c) && workqueue.off (commande)) {int recheck = ctl.get (); // rejeter si elle est exécutée par un statut de fonction (! Isrunning (reCheck) && retire (commande)) rejeter (Command); // Utilisez la stratégie spécifiée par le pool de threads pour rejeter la tâche // deux cas: // 1. Un nouvel état de course rejette de nouvelles tâches // 2. La file d'attente est complète et n'a pas réussi à démarrer un nouvel thread (WorkCount> MaximumpoHSIZE)} else if (!AddWorker:
Code simplifié
AddWorker booléen privé (Runnable FirstTask, Boolean Core) {int wc = workerCountOf (c); if (wc> = (core? CorepoolSize: maximumpoolSize)) {return false;} w = new Worker (FirstTask); Final Thread T = W.thread; t.start ();};Selon le code, examinons la situation susmentionnée de l'ajout de tâches pendant le travail de pool de fil:
* Si le nombre de threads en cours d'exécution est inférieur à CorePoolSize, créez un thread pour exécuter cette tâche immédiatement;
* Si le nombre de threads en cours d'exécution est supérieur ou égal à CorePoolSize, mettez cette tâche dans la file d'attente;
* Si la file d'attente est pleine pour le moment et que le nombre de threads en cours d'exécution est inférieur à MaximumpoolSize, vous devez toujours créer un thread non fondamental pour exécuter la tâche immédiatement;
* Si la file d'attente est pleine et que le nombre de threads en cours d'exécution est supérieur ou égal à MaximumpoolSize, le pool de threads lancera une exception RejectExecutionException.
C'est pourquoi Android AsyncTask est exécuté en parallèle et dépasse le nombre maximum de tâches et lance RejectExEcutionException. Pour plus de détails, veuillez vous référer à la dernière version d'AsyncTask Interprétation du code source et du côté obscur d'AsyncTask
Si un nouveau thread est créé avec succès via AddWorker, start () et utilisez FirstTask comme première tâche exécutée dans run () dans ce travailleur.
Bien que la tâche de chaque travailleur soit un traitement en série, si plusieurs travailleurs sont créés, car ils partagent un travail de travail, ils seront traités en parallèle.
Par conséquent, le nombre de concurrence maximum est contrôlé selon CorePoolSize et MaximumpoolSize. Le processus général peut être représenté par la figure ci-dessous.
L'explication et les images ci-dessus peuvent être bien comprises.
Si vous êtes engagé dans le développement d'Android et que vous connaissez les principes du gestionnaire, vous pouvez penser que cette image est assez familière. Certains processus sont très similaires à ceux utilisés par le gestionnaire, le looper et le meaasge. Handler.send (message) est équivalent à exécuter (runnuble). La file d'attente Meaasge maintenue dans LOOPER équivaut à BlockingQueue. Cependant, vous devez maintenir cette file d'attente par synchronisation. La fonction LOOP () dans Looper Loops pour prendre Meaasge de la file d'attente Meaasge et le runwork () dans le travailleur devient continuellement à partir de BlockingQueue.
3. Gérer les fils
Grâce au pool de threads, nous pouvons gérer la réutilisation du thread, contrôler le numéro de concurrence et détruire les processus. La réutilisation et le contrôle de la concurrence ont été discutés ci-dessus, et le processus de gestion du thread a été entrecoupé, ce qui est facile à comprendre.
Il existe une variable CTL ATOMICInteger dans ThreadPoolExecutor. Deux contenus sont enregistrés via cette variable:
Le nombre de tous les threads. Chaque fil est dans un état où les 29 bits inférieurs de fils sont stockés et les 3 bits plus élevés de RunState sont stockés. Différentes valeurs sont obtenues par le biais d'opérations de bit.
ATOMICInteger final privé ctl = new atomicInteger (ctlof (en cours d'exécution, 0)); // Obtenez l'état du thread privé static int runstateof (int c) {return c & ~ capacity;} // obtient le nombre de travailleurs statics privés en INT WorkerCountOf (int c) {return c & capacity;} // le nombre de travailleurs static Capacité;} // terme si le thread exécute un booléen statique privé Isrunning (int c) {return c <shutdown;}Ici, le processus d'arrêt du pool de threads est principalement analysé par Shutdown et ShutdownNow (). Tout d'abord, le pool de threads dispose de cinq états pour contrôler l'ajout et l'exécution des tâches. Les trois types principaux suivants sont introduits:
État d'exécution: le pool de threads s'exécute normalement et peut accepter de nouvelles tâches et des tâches de processus dans la file d'attente;
État d'arrêt: aucune nouvelle tâche n'est acceptée, mais les tâches dans la file d'attente seront exécutées;
Statut d'arrêt: aucune nouvelle tâche n'est acceptée et la fermeture de la tâche n'est pas traitée dans la file d'attente. Cette méthode définira RunState sur l'arrêt et mettra fin à tous les threads inactifs, tandis que les threads qui fonctionnent encore ne sont pas affectés, de sorte que la personne de tâche dans la file d'attente sera exécutée.
La méthode ShutdownNow définit RunState pour s'arrêter. La différence entre la méthode d'arrêt, cette méthode terminera tous les threads, de sorte que les tâches de la file d'attente ne seront pas exécutées.
Résumé: Grâce à l'analyse du code source ThreadPoolExecutor, nous avons une compréhension générale du processus de création de pools de threads, d'ajouter des tâches et d'exécuter des pools de threads. Si vous connaissez ces processus, il sera plus facile d'utiliser des pools de threads.
La partie du contrôle de la concurrence qui en a tiré et l'utilisation du traitement des tâches des modèles productrices-consommation sera d'une grande aide pour comprendre ou résoudre d'autres problèmes connexes à l'avenir. Par exemple, le mécanisme du gestionnaire dans Android et la file d'attente Messager dans Looper sont également OK pour utiliser un Blookqueue pour le gérer. Ceci est le gain de lecture du code source.
Ce qui précède est l'information triant le pool de threads Java. Nous continuerons d'ajouter des informations pertinentes à l'avenir. Merci pour votre soutien pour ce site Web!