CodeGym /Blog Java /Random-FR /Gestion des fils. Le mot clé volatile et la méthode yield...
Auteur
Volodymyr Portianko
Java Engineer at Playtika

Gestion des fils. Le mot clé volatile et la méthode yield()

Publié dans le groupe Random-FR
Salut! Nous continuons notre étude du multithreading. Aujourd'hui, nous allons apprendre à connaître le volatilemot-clé et la yield()méthode. Plongeons dedans :)

Le mot-clé volatil

Lors de la création d'applications multithreads, nous pouvons rencontrer deux problèmes sérieux. Premièrement, lorsqu'une application multithread est en cours d'exécution, différents threads peuvent mettre en cache les valeurs des variables (nous en avons déjà parlé dans la leçon intitulée 'Utiliser volatile' ). Vous pouvez avoir la situation où un thread modifie la valeur d'une variable, mais un second thread ne voit pas le changement, car il travaille avec sa copie en cache de la variable. Naturellement, les conséquences peuvent être graves. Supposons que ce ne soit pas n'importe quelle ancienne variable, mais plutôt le solde de votre compte bancaire, qui commence soudainement à sauter de haut en bas au hasard :) Cela ne semble pas amusant, n'est-ce pas ? Deuxièmement, en Java, les opérations de lecture et d'écriture de tous les types primitifs,longdouble, sont atomiques. Eh bien, par exemple, si vous modifiez la valeur d'une intvariable sur un thread et que sur un autre thread vous lisez la valeur de la variable, vous obtiendrez soit son ancienne valeur, soit la nouvelle, c'est-à-dire la valeur résultant du changement dans le fil 1. Il n'y a pas de 'valeurs intermédiaires'. Cependant, cela ne fonctionne pas avec longs et doubles. Pourquoi? En raison du support multiplateforme. Vous souvenez-vous au début que nous avons dit que le principe directeur de Java était "écrire une fois, exécuter n'importe où" ? Cela signifie un support multiplateforme. En d'autres termes, une application Java s'exécute sur toutes sortes de plates-formes différentes. Par exemple, sur les systèmes d'exploitation Windows, différentes versions de Linux ou MacOS. Il fonctionnera sans accroc sur chacun d'eux. Pesant dans un 64 bits,longdoublesont les primitives "les plus lourdes" de Java. Et certaines plates-formes 32 bits n'implémentent tout simplement pas la lecture et l'écriture atomiques des variables 64 bits. Ces variables sont lues et écrites en deux opérations. Tout d'abord, les 32 premiers bits sont écrits dans la variable, puis 32 autres bits sont écrits. En conséquence, un problème peut survenir. Un thread écrit une valeur 64 bits dans une Xvariable et le fait en deux opérations. Dans le même temps, un deuxième thread essaie de lire la valeur de la variable et le fait entre ces deux opérations - lorsque les 32 premiers bits ont été écrits, mais pas les 32 seconds. En conséquence, il lit une valeur intermédiaire incorrecte et nous avons un bogue. Par exemple, si sur une telle plate-forme nous essayons d'écrire le numéro à un 9223372036854775809 à une variable, il occupera 64 bits. Sous forme binaire, cela ressemble à ceci : 1000000000000000000000000000000000000000000000000000000001 Le premier thread commence à écrire le nombre dans la variable. Au début, il écrit les 32 premiers bits (100000000000000000000000000000) puis les 32 seconds (000000000000000000000000000001) Et le deuxième thread peut se coincer entre ces opérations, lisant la valeur intermédiaire de la variable (1000000000000000000000000000000), qui sont les 32 premiers bits qui ont déjà été écrits. Dans le système décimal, ce nombre est 2 147 483 648. En d'autres termes, nous voulions juste écrire le nombre 9223372036854775809 dans une variable, mais du fait que cette opération n'est pas atomique sur certaines plateformes, nous avons le nombre maléfique 2 147 483 648, qui est sorti de nulle part et aura un effet inconnu le programme. Le deuxième thread a simplement lu la valeur de la variable avant qu'elle ait fini d'être écrite, c'est-à-dire que le thread a vu les 32 premiers bits, mais pas les 32 seconds. Bien entendu, ces problèmes ne se sont pas posés hier. Java les résout avec un seul mot-clé : volatile. Si nous utilisons levolatilemot-clé lors de la déclaration d'une variable dans notre programme…

public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…cela signifie que:
  1. Il sera toujours lu et écrit de manière atomique. Même si c'est un 64 bits doubleou long.
  2. La machine Java ne le mettra pas en cache. Ainsi, vous n'aurez pas une situation où 10 threads travaillent avec leurs propres copies locales.
Ainsi, deux problèmes très sérieux sont résolus avec un seul mot :)

La méthode yield()

Nous avons déjà passé en revue de nombreuses Threadméthodes de la classe, mais il y en a une importante qui sera nouvelle pour vous. C'est la yield()méthode . Et il fait exactement ce que son nom l'indique ! Gestion des fils.  Le mot clé volatile et la méthode yield() - 2Lorsque nous appelons la yieldméthode sur un thread, elle communique en fait avec les autres threads : "Hé, les gars. Je ne suis pas particulièrement pressé d'aller quelque part, donc s'il est important pour l'un d'entre vous d'obtenir du temps processeur, prenez-le - je peux attendre ». Voici un exemple simple de la façon dont cela fonctionne :

public class ThreadExample extends Thread {

   public ThreadExample() {
       this.start();
   }

   public void run() {

       System.out.println(Thread.currentThread().getName() + " yields its place to others");
       Thread.yield();
       System.out.println(Thread.currentThread().getName() + " has finished executing.");
   }

   public static void main(String[] args) {
       new ThreadExample();
       new ThreadExample();
       new ThreadExample();
   }
}
Nous créons et démarrons séquentiellement trois threads : Thread-0, Thread-1et Thread-2. Thread-0commence le premier et cède aussitôt aux autres. Alors Thread-1est commencé et cède aussi. Alors Thread-2est commencé, qui cède aussi. Nous n'avons plus de threads, et après Thread-2avoir cédé sa place en dernier, le planificateur de threads dit : 'Hmm, il n'y a plus de nouveaux threads. Qui avons-nous dans la file d'attente ? Qui a cédé sa place avant Thread-2? Il paraît que c'était Thread-1. D'accord, cela signifie que nous allons le laisser fonctionner ». Thread-1termine son travail, puis l'ordonnanceur de threads poursuit sa coordination : 'D'accord, Thread-1c'est terminé. Avons-nous quelqu'un d'autre dans la file d'attente ?'. Thread-0 est dans la file d'attente : il a cédé sa place juste avantThread-1. Il obtient maintenant son tour et s'exécute jusqu'à la fin. Ensuite, l'ordonnanceur finit de coordonner les threads : 'D'accord, Thread-2, vous avez cédé aux autres threads, et ils sont tous terminés maintenant. Tu as été le dernier à céder, alors maintenant c'est ton tour'. Puis Thread-2s'exécute jusqu'à la fin. La sortie de la console ressemblera à ceci : Thread-0 cède sa place à d'autres Thread-1 cède sa place à d'autres Thread-2 cède sa place à d'autres Thread-1 a fini de s'exécuter. Thread-0 a fini de s'exécuter. Thread-2 a fini de s'exécuter. Bien sûr, le planificateur de threads peut démarrer les threads dans un ordre différent (par exemple, 2-1-0 au lieu de 0-1-2), mais le principe reste le même.

Règles de l'événement qui se passe avant

La dernière chose que nous aborderons aujourd'hui est le concept de « se produit avant ». Comme vous le savez déjà, en Java, le planificateur de threads effectue l'essentiel du travail impliqué dans l'allocation de temps et de ressources aux threads pour effectuer leurs tâches. Vous avez également vu à plusieurs reprises comment les threads sont exécutés dans un ordre aléatoire qui est généralement impossible à prévoir. Et en général, après la programmation "séquentielle" que nous avons faite précédemment, la programmation multithread ressemble à quelque chose d'aléatoire. Vous en êtes déjà venu à croire que vous pouvez utiliser une multitude de méthodes pour contrôler le flux d'un programme multithread. Mais le multithreading en Java a un autre pilier - les 4 règles " se produit avant ". Comprendre ces règles est assez simple. Imaginez que nous ayons deux threads - AetB. Chacun de ces threads peut effectuer des opérations 1et 2. Dans chaque règle, lorsque nous disons « A se produit avant B », nous voulons dire que toutes les modifications apportées par le thread Aavant l'opération 1et les modifications résultant de cette opération sont visibles pour le thread Blorsque l'opération 2est effectuée et par la suite. Chaque règle garantit que lorsque vous écrivez un programme multithread, certains événements se produiront avant d'autres 100% du temps, et qu'au moment de l'opération, le 2thread Bsera toujours au courant des modifications Aapportées par le thread pendant l'opération 1. Passons-les en revue.

Règle 1.

La libération d'un mutex se produit avant que le même moniteur ne soit acquis par un autre thread. Je pense que vous comprenez tout ici. Si le mutex d'un objet ou d'une classe est acquis par un thread, par exemple, par thread A, un autre thread (thread B) ne peut pas l'acquérir en même temps. Il doit attendre que le mutex soit libéré.

Règle 2.

La Thread.start()méthode se passe avant Thread.run() . Encore une fois, rien de difficile ici. Vous savez déjà que pour commencer à exécuter le code à l'intérieur de la run()méthode, vous devez appeler la start()méthode sur le thread. Plus précisément, la méthode start, pas la run()méthode elle-même ! Cette règle garantit que les valeurs de toutes les variables définies avant Thread.start()l'appel seront visibles dans la run()méthode une fois qu'elle aura commencé.

Règle 3.

La fin de la run()méthode se produit avant le retour de la join()méthode. Revenons à nos deux threads : Aet B. Nous appelons la join()méthode afin que le thread Bsoit assuré d'attendre la fin du thread Aavant de faire son travail. Cela signifie que la méthode de l'objet A run()est garantie de s'exécuter jusqu'à la fin. Et toutes les modifications apportées aux données qui se produisent dans la run()méthode de thread Asont garanties à cent pour cent d'être visibles dans le thread Bune fois qu'il est terminé en attendant que le thread Atermine son travail afin qu'il puisse commencer son propre travail.

Règle 4.

L'écriture dans une volatilevariable se produit avant la lecture à partir de cette même variable. Lorsque nous utilisons le volatilemot-clé, nous obtenons en fait toujours la valeur actuelle. Même avec un longou double(nous avons parlé plus tôt des problèmes qui peuvent arriver ici). Comme vous l'avez déjà compris, les modifications apportées à certains threads ne sont pas toujours visibles pour les autres threads. Mais, bien sûr, il y a des situations très fréquentes où un tel comportement ne nous convient pas. Supposons que nous attribuons une valeur à une variable sur thread A:

int z;

….

z = 555;
Si notre Bthread devait afficher la valeur de la zvariable sur la console, il pourrait facilement afficher 0, car il ne connaît pas la valeur assignée. Mais la règle 4 garantit que si nous déclarons la zvariable comme volatile, alors les changements de sa valeur sur un thread seront toujours visibles sur un autre thread. Si on ajoute le mot volatileau code précédent...

volatile int z;

….

z = 555;
... alors nous empêchons la situation où le thread Bpourrait afficher 0. L'écriture dans volatileles variables se produit avant la lecture de celles-ci.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION