Programmation séquentielle , c'est-à-dire que tout dans le programme ne peut effectuer qu'une seule étape à la fois. Programmation simultanée , le programme peut exécuter plusieurs parties du programme en parallèle.
Les threads peuvent piloter des tâches, vous avez donc besoin d'un moyen de décrire les tâches, qui peut être fourni par l'interface Runnable
. Pour définir une tâche, implémentez simplement l'interface Runnable
et écrivez la méthode run()
afin que la tâche puisse exécuter vos commandes.
Lorsqu'une classe est dérivée de Runnable
, elle doit avoir une méthode run()
, mais cette méthode n'a rien de spécial - elle ne produit aucune capacité de thread inhérente. Pour implémenter un comportement threadé, vous devez attacher explicitement une tâche à un fil .
FixedThreadPool
et CachedThreadPool
FixedThreadPool
peuvent pré-exécuter une allocation de thread coûteuse en une seule fois , le nombre de threads peut donc être limité. Cela permet de gagner du temps car vous n'avez pas à payer les frais généraux liés à la création d'un thread pour chaque tâche. Dans un système piloté par les événements, les gestionnaires d'événements qui nécessitent des threads peuvent également être servis à votre guise en récupérant les threads directement à partir du pool. Vous n'abuserez pas des ressources disponibles car le nombre d'objets Thread utilisés par FixedThreadPool est limité.
Notez que dans n'importe quel pool de threads, les threads existants seront automatiquement réutilisés lorsque cela est possible.
Bien que ce livre utilise CachedThreadPool
, vous devriez également envisager d'utiliser FiexedThreadPool
dans le code qui génère des fils de discussion. CachedThreadPool
Crée généralement autant de threads que nécessaire pendant l'exécution du programme, puis arrête de créer de nouveaux threads lorsqu'il recycle les anciens threads, c'est donc un Executor
premier choix raisonnable. Vous ne devez passer à FixedThreadPool
que si cette méthode pose des problèmes.
SingleThreadExecutor
c'est comme 1
avec un nombre de fils FixedThreadPool
. (Cela fournit également une garantie de concurrence importante qu'aucun autre thread (c'est-à-dire qu'il n'y aura pas deux threads) ne sera appelé. Cela modifiera les exigences de verrouillage de la tâche)
Si plusieurs tâches sont soumises à SingleThreadExecutor
, alors les tâches seront mise en file d'attente, chaque tâche s'exécutera jusqu'à la fin avant le démarrage de la tâche suivante et toutes les tâches utiliseront le même thread. Dans l'exemple ci-dessous, vous pouvez voir que chaque tâche est terminée dans l'ordre dans lequel elle a été soumise et avant le début de la tâche suivante. Par conséquent, SingleThreadExecutor
sérialisera toutes les tâches qui lui sont soumises et maintiendra sa propre file d'attente (cachée) de tâches en attente.
Runnable
est une tâche indépendante qui effectue un travail, mais elle ne renvoie pas de valeur de tâche. Si vous souhaitez que la tâche renvoie une valeur une fois terminée, vous pouvez implémenter l'interface Callable
au lieu de l'interface Runnable
. Le Callable
introduit dans Java SE5 est un type générique avec un paramètre de type. Son paramètre de type représente la valeur renvoyée par la méthode call()
(au lieu de run()
), et la méthode ExecutorService.submit()
doit être utilisée.
Un autre idiome que vous pourriez voir est le Runnable
autogéré.
Ceci n'est pas particulièrement différent de l'héritage de Thread
, sauf que la syntaxe est un peu plus obscure. Cependant, l'implémentation d'une interface vous permet d'hériter d'une classe différente, alors que l'héritage de Thread
ne le fera pas.
Notez que le Runnable
autogéré est appelé dans le constructeur. Cet exemple est assez simple et donc probablement sûr, mais vous devez savoir que démarrer un thread dans un constructeur peut devenir problématique car une autre tâche peut commencer à s'exécuter avant la fin du constructeur, ce qui signifie que la tâche peut accéder aux objets dans un état instable. C'est une autre raison de préférer Executor
à la création explicite d'un objet Thread对
.
Le groupe de discussions contient une collection de discussions. La valeur des groupes de threads peut être résumée en citant Joshua Bloch : « Il est préférable de considérer les groupes de threads comme une tentative infructueuse que vous pouvez simplement ignorer
.
Si vous avez consacré beaucoup de temps et d'efforts à essayer de découvrir la valeur des groupes de discussion (comme moi), vous pourriez être surpris de savoir pourquoi il n'y a pas eu de déclaration officielle de Sun. sur le sujet depuis des années. La même question a été posée d'innombrables fois à propos d'autres changements apportés à Java depuis lors. La philosophie de vie de Joseph Stiglitz, lauréat du prix Nobel d'économie, peut être utilisée pour expliquer ce problème. Elle s'appelle la théorie de l'engagement croissant : « Le coût de continuer à commettre des erreurs sera supporté par d'autres, tandis que le coût de l'admission de ses erreurs. sera à la charge de soi-même. »
En raison de la nature des threads, vous ne pouvez pas détecter les exceptions qui s'échappent du thread. Une fois qu'une exception échappe à la méthode run()
d'une tâche, elle se propagera à la console à moins que vous ne preniez des mesures spéciales pour intercepter ces exceptions erronées.
Les programmes monothread peuvent être considérés comme une entité unique qui résout le domaine du problème et ne peut faire qu'une chose à la fois.
Parce que le drapeau canceled
est de type boolean
, il est atomique, c'est-à-dire que des opérations simples telles que l'affectation et la valeur de retour sont aucune possibilité d'interruption pendant que cela se produit, vous ne verrez donc pas le domaine dans un état intermédiaire en train d'effectuer ces opérations simples.
Il est important de noter que la procédure d'incrémentation elle-même nécessite plusieurs étapes et que la tâche peut être suspendue par le mécanisme pur pendant le processus d'incrémentation - c'est-à-dire qu'en Java, l'incrémentation n'est pas une opération atomique. Par conséquent, même un seul incrément n’est pas sûr sans protéger la tâche.
et appeler Executor
sur shutdownNow()
enverra un appel interrupt()
à tous les fils de discussion qu'il démarre.
Executor
En appelant submit()
au lieu de excutor()
pour démarrer une tâche, vous pouvez conserver le contexte de la tâche. submit()
renverra un Future<?>
générique, la clé pour avoir ce Future
est que vous pouvez appeler cancel()
dessus et donc l'utiliser pour interrompre une tâche spécifique. Si vous transmettez true
à cancel()
, alors il aura la permission d'appeler interrupt()
sur ce fil pour l'arrêter. Par conséquent, cancel()
est un moyen d'interrompre un seul fil de discussion démarré par Excutor
.
SleepBlock()
est un blocage interrompu, tandis que IOBlocked
et SynchronizedBlocked
sont un blocage ininterruptible. Les exemples des trois classes ci-dessus prouvent que les E/S et l'attente des blocs synchronized
sont ininterruptibles. Ni les E/S ni la tentative d'appel d'une méthode synchronized
ne nécessitent de gestionnaire InterruptedException
.
Comme vous pouvez le voir dans le résultat de l'exemple pour les trois classes ci-dessus, vous pouvez interrompre l'appel à sleep()
(ou tout appel nécessitant le lancement de InterruptedException
). Cependant, vous ne pouvez pas interrompre un thread qui tente d'acquérir un verrou synchronized
ou qui tente d'effectuer une opération d'E/S. C'est un peu ennuyeux, surtout lorsque vous effectuez des tâches d'E/S, car cela signifie que IO a le potentiel de verrouiller votre programme multithread. Surtout pour les programmes basés sur le Web, c'est une question d'enjeux.
Pour ce genre de problème, il existe une solution un peu maladroite mais parfois efficace, qui consiste à fermer la ressource sous-jacente sur laquelle la tâche est bloquée :
wait()
vous permet d'attendre qu'une certaine condition change, et la modification de cette condition échappe au contrôle de la méthode actuelle. Souvent, cette condition sera modifiée par une autre tâche. Vous ne voulez certainement pas continuer à faire une boucle vide pendant que votre tâche teste cette condition. C'est ce qu'on appelle une attente occupée et constitue généralement une mauvaise utilisation des cycles. Par conséquent, wait()
suspendra la tâche en attendant des changements dans le monde extérieur, et seulement lorsque notify()
ou notifyAll()
se produit, ce qui signifie que quelque chose d'intéressant s'est produit, la tâche sera réveillée et pour vérifier les changements effectués . Par conséquent, wait()
fournit un moyen de synchroniser les activités entre les tâches.
Le verrou ne se libère pas lors de l'appel de sleep()
C'est également le cas lors de l'appel de yield()
. Il est important de comprendre cela. Un aspect particulier de wait()
, notify()
et notifyAll()
est que ces méthodes font partie de la classe de base Object
, et non de Thread
.
Signal manqué.
Dans la discussion sur le mécanisme de threading de Java, il y a une description confuse : notifyAll()
réveillera "toutes les tâches en attente dans le futur" " . Cela signifie-t-il que toute tâche dans l'état wait()
n'importe où dans le programme sera réveillée par tout appel à notifyAll()
? Il existe des exemples où ce n'est pas le cas - en fait, lorsque notifyAll()
est appelé pour un verrou spécifique, seules les tâches en attente de ce verrou seront réveillées.
Le problème des philosophes de la restauration proposé par Edsger Dijkstrar est un exemple classique d'impasse.
Pour résoudre le problème de blocage, vous devez comprendre qu'un blocage se produira lorsque les quatre conditions suivantes sont remplies en même temps :
Condition mutuellement exclusive. Au moins une des ressources utilisées par la tâche ne peut pas être partagée. Ici, une baguette ne peut être utilisée que par un seul philosophe à la fois.
Au moins une tâche doit détenir une ressource et est en attente d'acquérir une ressource actuellement détenue par une autre tâche. Autrement dit, pour qu’une impasse se produise, le philosophe doit tenir une baguette et en attendre une autre.
Les ressources ne peuvent pas être préemptées par les tâches, et les tâches doivent traiter la libération des ressources comme un événement normal. Les philosophes sont polis et ne prennent pas les baguettes des autres philosophes.
Il doit y avoir une boucle d'attente. A ce moment, une tâche attend les ressources détenues par d'autres tâches, et cette dernière attend la pulpe détenue par une autre tâche. continue jusqu'à ce qu'une tâche attende les ressources détenues par la première tâche, ce qui entraîne le verrouillage de tout le monde. Dans DeadlockingDiningPhilosophers.java, parce que chaque philosophe essaie d'abord de placer la baguette à droite, puis la baguette à gauche, une attente en boucle se produit.
Donc, pour éviter une impasse, il vous suffit d'en détruire un. Le moyen le plus simple d’éviter un blocage est de violer la condition 4.
Scénarios applicables : Il est utilisé pour synchroniser une ou plusieurs tâches, les obligeant à attendre leur exécution par d'autres tâches Un ensemble d'opérations est réalisé. Autrement dit, une ou plusieurs tâches doivent attendre que d’autres tâches, telles que la partie initiale d’un problème, soient terminées.
Vous pouvez définir une valeur initiale pour l'objet CountDownLatch
. Toute méthode appelant wait() sur cet objet se bloquera jusqu'à ce que la valeur du compteur atteigne 0. Lorsque d'autres facteurs terminent leur travail, ils peuvent appeler countDown(). sur l'objet pour décrémenter le décompte. CountDownLatch
Conçu pour être tiré une seule fois et le décompte ne peut pas être réinitialisé. Si vous avez besoin d'une version qui réinitialise le décompte, vous pouvez utiliser CyclicBarrier
.
La tâche appelant countDown()
n'est pas bloquée lorsque cet appel est effectué. Seul l'appel à await()
sera bloqué jusqu'à ce que la valeur du compteur atteigne 0
. L'utilisation typique de
CountDownLatch
est de diviser un programme en n
tâches indépendantes résolubles et de créer n
avec une valeur CountDownLatch
. Lorsque chaque tâche est terminée, countDown()
est appelé sur ce verrou. Les tâches en attente de résolution du problème appellent await()
sur ce verrou, se suspendant jusqu'à la fin du décompte des verrous.
Convient aux situations où vous souhaitez créer un ensemble de tâches qui effectuent un travail en parallèle, puis attendre que toutes les tâches soient terminées avant de passer à l'étape suivante (ressemble à un peu comme Join()). Cela fait que toutes les tâches parallèles sont mises en file d'attente au niveau de la clôture et avancent donc uniformément.
Par exemple, le programme de courses de chevaux : HorseRace.java
DelayQueue
est un BlockingQueue
(file d'attente synchrone) illimité, utilisé pour placez l'implémentation Delayed
L'objet de l'interface, l'objet dans lequel ne peut être extrait de la file d'attente que lorsqu'il expire. Cette file d'attente est ordonnée, c'est-à-dire que l'objet principal est le premier à expirer. S'il n'y a pas d'objets expirés, alors la file d'attente n'a pas d'élément head, donc poll()
renverra null
(pour cette raison, nous ne pouvons pas placer null
dans ce type de file d'attente). Comme mentionné ci-dessus, DelayQueue
devient une variante de file d'attente prioritaire.
Il s'agit d'une file d'attente prioritaire très basique avec des opérations de lecture bloquantes. La nature bloquante de cette file d'attente fournit toute la synchronisation nécessaire, vous devez donc noter qu'il n'y a pas besoin de synchronisation explicite ici - vous n'avez pas à vous soucier de savoir s'il y a des éléments dans cette file d'attente lorsque vous la lisez, car cela file d'attente Lorsqu'il n'y a aucun élément, le lecteur sera directement bloqué.
Le « système de contrôle de serre » peut être considéré comme un problème de concurrence, chaque événement de serre souhaité est une tâche qui s'exécute à une heure planifiée. ScheduledThreadPoolExecutor
peut résoudre ce problème. Parmi eux, planning() est utilisé pour exécuter la tâche une fois, et planningAtFixedRate() exécute la tâche à plusieurs reprises à chaque heure spécifiée. Les deux méthodes reçoivent le paramètre delayTime. Les objets exécutables peuvent être configurés pour s'exécuter à un moment donné dans le futur.
BlockingQueue
: File d'attente synchrone, lorsque le premier élément est vide ou indisponible, attendez (blocage, Blocage) lors de l'exécution de .take().
SynchronousQueue
: est une file d'attente bloquante sans capacité interne, donc chaque put() doit attendre une take(), et vice versa (c'est-à-dire que chaque take() doit attendre une put() )) . C'est comme si vous tendiez un objet à quelqu'un : il n'y a pas de table sur laquelle placer l'objet, vous ne pouvez donc travailler que si la personne tend la main et est prête à recevoir l'objet. Dans ce cas, SynchronousQueue représente un emplacement placé devant le restaurant pour renforcer le concept selon lequel un seul plat peut être servi à tout moment.
Une chose très importante à observer à propos de cet exemple est la complexité administrative liée à l'utilisation de files d'attente pour communiquer entre les tâches. Cette technique unique simplifie grandement le processus de programmation simultanée en inversant le contrôle : les tâches n'interfèrent pas directement les unes avec les autres, mais s'envoient plutôt des objets via des files d'attente. La tâche de réception gère l'objet et le traite comme un message plutôt que de lui envoyer des messages. Si vous suivez cette technique autant que possible, vos chances de construire un système concurrent robuste sont considérablement augmentées.
Les dangers du « microbenchmarking » : Ce terme fait généralement référence à tests de performances d’une fonctionnalité de manière isolée et hors contexte. Bien sûr, vous devez toujours écrire des tests pour vérifier des assertions telles que « Le verrouillage est plus rapide que synchronisé », mais vous devez être conscient de ce qui se passe réellement lors de la compilation et de l'exécution lors de l'écriture de ces tests.
Différents compilateurs et systèmes d'exécution varient à cet égard, il est donc difficile de savoir exactement ce qui va se passer, mais nous devons empêcher le compilateur de prédire la possibilité du résultat.
L'utilisation de Lock est généralement beaucoup plus efficace que l'utilisation de synchronisé, et la surcharge de synchronisé semble trop varier, alors que Lock est relativement cohérent.
Cela signifie-t-il que vous ne devriez jamais utiliser le mot-clé synchronisé ? Il y a deux facteurs à considérer ici :
Premièrement, la taille du corps de la méthode mutuellement exclusive.
Deuxièmement, le code généré par le mot-clé synchronisé est plus lisible que le code généré par l'idiome "lock-try/finally-unlock" requis par Lock.
Le code est lu beaucoup plus souvent qu'il n'est écrit. Lors de la programmation, la communication avec d’autres personnes est bien plus importante que la communication avec l’ordinateur, la lisibilité de votre code est donc cruciale. Par conséquent, il est d’une importance pratique de commencer par le mot-clé synchronisé et de le remplacer uniquement par un objet Lock lors du réglage des performances.
La stratégie générale pour ces fenêtres sans verrouillage est la suivante : les modifications du conteneur peuvent se produire simultanément avec les opérations de lecture, à condition que le lecteur seul Vous pouvez voir les résultats des modifications complétées. Les modifications sont effectuées sur une copie distincte d'une partie de la structure de données du conteneur (parfois une copie de la structure de données entière), et cette copie n'est pas visible pendant le processus de modification. Ce n'est que lorsque la modification est terminée que la structure modifiée est automatiquement échangée avec la structure de données principale, et le lecteur peut alors voir la modification.
Verrouillage optimiste
Tant que vous lisez principalement à partir d'un conteneur sans verrou, il sera beaucoup plus rapide que son homologue synchronisé, car la surcharge liée à l'acquisition et à la libération des verrous est éliminée. C'est toujours le cas si un petit nombre d'écritures doit être effectué dans un conteneur sans verrouillage, mais qu'est-ce qui compte comme une « petite quantité » ? C'est une question très intéressante.
Un avantage supplémentaire des threads est qu'ils fournissent des commutateurs de contexte d'exécution légers (environ 100 instructions) plutôt que des commutateurs de contexte de processus lourds (des milliers d'instructions). Étant donné que tous les threads d'un processus donné partagent le même espace mémoire, le changement de contexte léger modifie uniquement la séquence d'exécution du programme et les variables locales. Les commutateurs de processus (commutateurs de contexte lourds) doivent modifier tout l’espace mémoire.
Articles connexes :
Cours d'apprentissage des pensées de programmation Java (6) Chapitre 19 - Types énumérés
Cours d'apprentissage des pensées de programmation Java (7) Chapitre 20 - Annotations
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!