Java 101: Comprendre les threads Java, Partie 3: Planification des threads et attente / notification

Ce mois-ci, je continue mon introduction en quatre parties aux threads Java en me concentrant sur la planification des threads, le mécanisme d'attente / notification et l'interruption des threads. Vous allez étudier comment une JVM ou un planificateur de threads du système d'exploitation choisit le prochain thread à exécuter. Comme vous le découvrirez, la priorité est importante dans le choix d'un programmateur de threads. Vous allez examiner comment un thread attend jusqu'à ce qu'il reçoive une notification d'un autre thread avant de continuer son exécution et apprendre à utiliser le mécanisme d'attente / notification pour coordonner l'exécution de deux threads dans une relation producteur-consommateur. Enfin, vous apprendrez à réveiller prématurément un thread en veille ou en attente pour l'arrêt du thread ou d'autres tâches. Je vais également vous apprendre comment un thread qui n'est ni en veille ni en attente détecte une demande d'interruption d'un autre thread.

Notez que cet article (qui fait partie des archives JavaWorld) a été mis à jour avec de nouvelles listes de codes et du code source téléchargeable en mai 2013.

Comprendre les threads Java - lire toute la série

  • Partie 1: Présentation des threads et des exécutables
  • Partie 2: Synchronisation
  • Partie 3: Planification des threads, attente / notification et interruption des threads
  • Partie 4: Groupes de threads, volatilité, variables locales de thread, minuteries et mort de thread

Planification des threads

Dans un monde idéalisé, tous les threads de programme auraient leurs propres processeurs sur lesquels s'exécuter. Jusqu'au moment où les ordinateurs ont des milliers ou des millions de processeurs, les threads doivent souvent partager un ou plusieurs processeurs. Soit la machine virtuelle Java, soit le système d'exploitation de la plate-forme sous-jacente décrypte comment partager la ressource processeur entre les threads, une tâche appelée planification des threads . La partie de la JVM ou du système d'exploitation qui effectue la planification des threads est un planificateur de threads .

Remarque: pour simplifier ma discussion sur la planification des threads, je me concentre sur la planification des threads dans le contexte d'un seul processeur. Vous pouvez extrapoler cette discussion à plusieurs processeurs; Je vous laisse cette tâche.

Rappelez-vous deux points importants sur la planification des threads:

  1. Java ne force pas une machine virtuelle à planifier des threads d'une manière spécifique ou à contenir un planificateur de threads. Cela implique une planification des threads dépendante de la plate-forme. Par conséquent, vous devez faire preuve de prudence lors de l'écriture d'un programme Java dont le comportement dépend de la façon dont les threads sont planifiés et doit fonctionner de manière cohérente sur différentes plates-formes.
  2. Heureusement, lors de l'écriture de programmes Java, vous devez penser à la façon dont Java planifie les threads uniquement lorsqu'au moins un des threads de votre programme utilise fortement le processeur pendant de longues périodes et que les résultats intermédiaires de l'exécution de ce thread s'avèrent importants. Par exemple, une applet contient un thread qui crée dynamiquement une image. Régulièrement, vous voulez que le fil de peinture dessine le contenu actuel de cette image afin que l'utilisateur puisse voir la progression de l'image. Pour vous assurer que le thread de calcul ne monopolise pas le processeur, envisagez la planification des threads.

Examinez un programme qui crée deux threads gourmands en ressources processeur:

Liste 1. SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemocrée deux threads qui calculent chacun la valeur de pi (cinq fois) et impriment chaque résultat. Selon la façon dont votre implémentation JVM planifie les threads, vous pouvez voir une sortie ressemblant à ce qui suit:

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

Selon la sortie ci-dessus, le planificateur de threads partage le processeur entre les deux threads. Cependant, vous pouvez voir une sortie similaire à celle-ci:

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

La sortie ci-dessus montre le planificateur de threads favorisant un thread par rapport à un autre. Les deux sorties ci-dessus illustrent deux catégories générales d'ordonnanceurs de threads: vert et natif. J'explorerai leurs différences de comportement dans les sections à venir. En discutant de chaque catégorie, je me réfère aux états de thread, dont il y en a quatre:

  1. État initial: un programme a créé l'objet thread d'un thread, mais le thread n'existe pas encore car la start()méthode de l'objet thread n'a pas encore été appelée.
  2. État exécutable: il s'agit de l'état par défaut d'un thread. Une fois l'appel à start()terminé, un thread devient exécutable, qu'il soit en cours d'exécution ou non, c'est-à-dire à l'aide du processeur. Bien que de nombreux threads puissent être exécutables, un seul s'exécute actuellement. Les planificateurs de threads déterminent le thread exécutable à attribuer au processeur.
  3. État bloqué: Lorsqu'un thread exécute les sleep(), wait()ou les join()méthodes, quand une tentative de fil pour lire les données non encore disponibles à partir d' un réseau, et lorsqu'un thread attend d'acquérir un verrou, ce fil est à l'état bloqué: il est ni en cours d' exécution , ni en mesure de courir. (Vous pouvez probablement penser à d'autres moments où un thread attendrait que quelque chose se produise.) Lorsqu'un thread bloqué se débloque, ce thread passe à l'état exécutable.
  4. État de fin: une fois que l'exécution quitte la run()méthode d' un thread , ce thread est dans l'état de fin. En d'autres termes, le fil cesse d'exister.

Comment le planificateur de threads choisit-il le thread exécutable à exécuter? Je commence à répondre à cette question tout en discutant de la planification des fils verts. Je termine la réponse en discutant de la planification des threads natifs.

Planification des fils verts

Tous les systèmes d'exploitation, par exemple l'ancien système d'exploitation Microsoft Windows 3.1, ne prennent pas en charge les threads. Pour de tels systèmes, Sun Microsystems peut concevoir une JVM qui divise son seul thread d'exécution en plusieurs threads. La JVM (et non le système d'exploitation de la plate-forme sous-jacente) fournit la logique de thread et contient le planificateur de thread. Les threads JVM sont des threads verts ou des threads utilisateur .

Le planificateur de threads d'une machine virtuelle Java planifie les threads verts en fonction de leur priorité , c'est-à-dire l'importance relative d'un thread, que vous exprimez sous la forme d'un entier à partir d'une plage de valeurs bien définie. En règle générale, le planificateur de threads d'une JVM choisit le thread de priorité la plus élevée et autorise ce thread à s'exécuter jusqu'à ce qu'il se termine ou se bloque. À ce moment-là, le planificateur de thread choisit un thread de la priorité la plus élevée suivante. Ce thread s'exécute (généralement) jusqu'à ce qu'il se termine ou se bloque. Si, pendant qu'un thread s'exécute, un thread de priorité plus élevée se débloque (peut-être que le temps de veille du thread de priorité plus élevée a expiré), le planificateur de thread anticipe ou interrompt le thread de priorité inférieure et attribue le thread de priorité supérieure débloqué au processeur.

Remarque: un thread exécutable avec la priorité la plus élevée ne s'exécutera pas toujours. Voici la priorité des spécifications du langage Java :

Chaque fil a une priorité. En cas de concurrence pour les ressources de traitement, les threads de priorité plus élevée sont généralement exécutés de préférence aux threads de priorité inférieure. Cependant, une telle préférence ne garantit pas que le thread de priorité la plus élevée sera toujours en cours d'exécution et les priorités de thread ne peuvent pas être utilisées pour implémenter de manière fiable l'exclusion mutuelle.

Cet aveu en dit long sur la mise en œuvre de JVM de threads verts. Ces JVM ne peuvent pas se permettre de laisser les threads se bloquer car cela lierait le seul fil d'exécution de la JVM. Par conséquent, lorsqu'un thread doit bloquer, par exemple lorsque ce thread lit des données lentement pour arriver à partir d'un fichier, la JVM peut arrêter l'exécution du thread et utiliser un mécanisme d'interrogation pour déterminer quand les données arrivent. Tant que le thread reste arrêté, le planificateur de thread de la JVM peut planifier l'exécution d'un thread de priorité inférieure. Supposons que les données arrivent pendant l'exécution du thread de priorité inférieure. Bien que le thread de priorité supérieure doive s'exécuter dès l'arrivée des données, cela ne se produit que lorsque la machine virtuelle Java interroge ensuite le système d'exploitation et découvre l'arrivée. Par conséquent, le thread de priorité inférieure s'exécute même si le thread de priorité supérieure doit s'exécuter.Vous devez vous soucier de cette situation uniquement lorsque vous avez besoin d'un comportement en temps réel de Java. Mais alors Java n'est pas un système d'exploitation en temps réel, alors pourquoi s'inquiéter?

Pour comprendre quel thread vert exécutable devient le thread vert en cours d'exécution, tenez compte de ce qui suit. Supposons que votre application se compose de trois threads: le thread principal qui exécute la main()méthode, un thread de calcul et un thread qui lit les entrées clavier. Lorsqu'il n'y a pas d'entrée au clavier, le thread de lecture se bloque. Supposons que le thread de lecture a la priorité la plus élevée et que le thread de calcul a la priorité la plus basse. (Par souci de simplicité, supposons également qu'aucun autre thread JVM interne n'est disponible.) La figure 1 illustre l'exécution de ces trois threads.

Au temps T0, le thread principal démarre. Au moment T1, le thread principal démarre le thread de calcul. Étant donné que le thread de calcul a une priorité inférieure à celle du thread principal, le thread de calcul attend le processeur. Au temps T2, le thread principal démarre le thread de lecture. Étant donné que le thread de lecture a une priorité plus élevée que le thread principal, le thread principal attend le processeur pendant que le thread de lecture s'exécute. Au moment T3, le thread de lecture se bloque et le thread principal s'exécute. Au temps T4, le thread de lecture se débloque et s'exécute; le thread principal attend. Enfin, au temps T5, le thread de lecture se bloque et le thread principal s'exécute. Cette alternance d'exécution entre les threads de lecture et principaux se poursuit tant que le programme s'exécute. Le thread de calcul ne s'exécute jamais car il a la priorité la plus basse et manque donc d'attention du processeur,une situation connue sous le nom dela famine du processeur .

Nous pouvons modifier ce scénario en donnant au thread de calcul la même priorité que le thread principal. La figure 2 montre le résultat, en commençant par le temps T2. (Avant T2, la figure 2 est identique à la figure 1.)

Au temps T2, le thread de lecture s'exécute pendant que les threads principal et de calcul attendent le processeur. Au moment T3, le thread de lecture se bloque et le thread de calcul s'exécute, car le thread principal s'exécutait juste avant le thread de lecture. Au temps T4, le thread de lecture se débloque et s'exécute; les threads principal et de calcul attendent. Au moment T5, le thread de lecture se bloque et le thread principal s'exécute, car le thread de calcul s'exécutait juste avant le thread de lecture. Cette alternance d'exécution entre les threads principal et de calcul se poursuit tant que le programme s'exécute et dépend du thread de priorité supérieure en cours d'exécution et de blocage.

We must consider one last item in green thread scheduling. What happens when a lower-priority thread holds a lock that a higher-priority thread requires? The higher-priority thread blocks because it cannot get the lock, which implies that the higher-priority thread effectively has the same priority as the lower-priority thread. For example, a priority 6 thread attempts to acquire a lock that a priority 3 thread holds. Because the priority 6 thread must wait until it can acquire the lock, the priority 6 thread ends up with a 3 priority—a phenomenon known as priority inversion.

L'inversion de priorité peut considérablement retarder l'exécution d'un thread de priorité plus élevée. Par exemple, supposons que vous ayez trois threads avec des priorités de 3, 4 et 9. Le thread de priorité 3 est en cours d'exécution et les autres threads sont bloqués. Supposons que le thread de priorité 3 attrape un verrou et que le thread de priorité 4 se débloque. Le thread de priorité 4 devient le thread en cours d'exécution. Étant donné que le thread de priorité 9 nécessite le verrou, il continue d'attendre jusqu'à ce que le thread de priorité 3 libère le verrou. Cependant, le thread de priorité 3 ne peut pas libérer le verrou tant que le thread de priorité 4 n'est pas bloqué ou ne se termine pas. En conséquence, le thread de priorité 9 retarde son exécution.