JavaRush /Blog Java /Random-FR /Gestion des flux. Le mot clé volatile et la méthode rende...

Gestion des flux. Le mot clé volatile et la méthode rendement()

Publié dans le groupe Random-FR
Bonjour! Nous continuons à étudier le multithreading et aujourd'hui, nous allons nous familiariser avec un nouveau mot-clé - volatile et la méthode rendement(). Voyons ce que c'est :)

Mot clé volatile

Lors de la création d’applications multithread, nous pouvons être confrontés à deux problèmes sérieux. Premièrement, lors du fonctionnement d'une application multithread, différents threads peuvent mettre en cache les valeurs des variables (nous en parlerons plus en détail dans la conférence « Utilisation de volatile » ). Il est possible qu'un thread ait modifié la valeur d'une variable, mais que le second n'ait pas vu ce changement car il travaillait avec sa propre copie en cache de la variable. Naturellement, les conséquences peuvent être graves. Imaginez qu'il ne s'agisse pas simplement d'une sorte de « variable », mais, par exemple, du solde de votre carte bancaire, qui se met soudain à osciller au hasard :) Pas très agréable, non ? Deuxièmement, en Java, les opérations de lecture et d'écriture sur les champs de tous types sauf longet doublesont atomiques. Qu’est-ce que l’atomicité ? Eh bien, par exemple, si vous modifiez la valeur d'une variable dans un thread intet que dans un autre thread vous lisez la valeur de cette variable, vous obtiendrez soit son ancienne valeur, soit une nouvelle - celle qui s'est avérée après le changement de fil 1. Aucune « options intermédiaires » n'y apparaîtra peut-être. Cependant, cela ne fonctionne pas avec longet . doublePourquoi? Parce que c'est multiplateforme. Vous souvenez-vous de la façon dont nous disions aux premiers niveaux que le principe Java est « écrit une fois, fonctionne partout » ? C’est multiplateforme. Autrement dit, une application Java s'exécute sur des plates-formes complètement différentes. Par exemple, sur les systèmes d'exploitation Windows, différentes versions de Linux ou MacOS, et partout, cette application fonctionnera de manière stable. longet double- les primitives les plus « lourdes » de Java : elles pèsent 64 bits. Et certaines plates-formes 32 bits n'implémentent tout simplement pas l'atomicité de lecture et d'écriture de 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. Par conséquent, dans ces cas, un problème peut survenir. Un thread écrit une valeur de 64 bits dans une variableХ, et il le fait « en deux étapes ». En même temps, le deuxième thread essaie de lire la valeur de cette variable, et le fait en plein milieu, lorsque les 32 premiers bits ont déjà été écrits, mais que les seconds ne l'ont pas encore été. En conséquence, il lit une valeur intermédiaire incorrecte et une erreur se produit. Par exemple, si sur une telle plate-forme nous essayons d'écrire un nombre dans une variable - 9223372036854775809 - elle occupera 64 bits. Sous forme binaire, cela ressemblera à ceci : 1000000000000000000000000000000000000000000000000000000001. Le premier thread commencera à écrire ce nombre dans une variable et écrira d'abord les 32 premiers bits : 1000000000000000000000000. 0000 00000 puis le deuxième 32 : 00000000000000000000000000000001 Et un deuxième fil peut se coincer dans cet interstice et lire la valeur intermédiaire de la variable - 1000000000000000000000000000000000, les 32 premiers bits déjà écrits. Dans le système décimal, ce nombre est égal à 2147483648. Autrement dit, nous voulions simplement écrire le nombre 9223372036854775809 dans une variable, mais du fait que cette opération sur certaines plates-formes n'est pas atomique, nous avons obtenu le nombre « gauche » 2147483648. , dont nous n'avons pas besoin, sorti de nulle part et on ne sait pas comment cela affectera le fonctionnement du programme. Le deuxième thread a simplement lu la valeur de la variable avant qu'elle ne soit finalement écrite, c'est-à-dire qu'il a vu les 32 premiers bits, mais pas les 32 seconds bits. Bien entendu, ces problèmes ne se sont pas posés hier et, en Java, ils sont résolus à l'aide d'un seul mot-clé - volatile . Si nous déclarons une variable dans notre programme avec le mot volatile...
public class Main {

   public volatile long x = 2222222222222222222L;

   public static void main(String[] args) {

   }
}
…cela signifie que:
  1. Il sera toujours lu et écrit atomiquement. Même si c'est 64 bits doubleou long.
  2. La machine Java ne le mettra pas en cache. Ainsi, la situation dans laquelle 10 threads fonctionnent avec leurs copies locales est exclue.
C'est ainsi que deux problèmes très sérieux sont résolus en un seul mot :)

Méthode rendement()

Nous avons déjà examiné de nombreuses méthodes de la classe Thread, mais il y en a une importante qui sera nouvelle pour vous. C'est la méthode rendement() . Traduit de l’anglais par « céder ». Et c’est exactement ce que fait la méthode ! Gestion des flux.  Le mot clé volatile et la méthode rendement() - 2Lorsque nous appelons la méthode rendement sur un thread, elle dit en fait aux autres threads : « D'accord, les gars, je ne suis pas particulièrement pressé, donc s'il est important pour l'un d'entre vous d'avoir du temps CPU, prenez-le, je suis pas urgent." 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() + "give way 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 lançons séquentiellement trois threads - Thread-0, Thread-1et Thread-2. Thread-0commence le premier et cède immédiatement la place aux autres. Après cela Thread-1, il démarre et cède également. Après cela, ça démarre Thread-2, ce qui est également inférieur. Nous n'avons plus de threads, et après que Thread-2le dernier a cédé sa place, le planificateur de threads regarde : « Alors, il n'y a plus de nouveaux threads, qui avons-nous dans la file d'attente ? Qui a été le dernier à céder sa place auparavant Thread-2? Je pense que c'était Thread-1? D'accord, alors laisse faire. Thread-1fait son travail jusqu'à la fin, après quoi le planificateur de threads continue de se coordonner : « D'accord, Thread-1 est terminé. Est-ce qu'il y a quelqu'un d'autre en ligne ? » Il y a le Thread-0 dans la file d'attente : il a cédé sa place juste avant le Thread-1. Maintenant, l'affaire lui est venue à l'esprit et il est mené jusqu'au bout. Après quoi le planificateur termine de coordonner les threads : « D'accord, Thread-2, vous avez cédé la place à d'autres threads, ils ont tous déjà fonctionné. Vous avez été le dernier à céder, alors maintenant c'est votre tour. Après cela, Thread-2 s'exécute jusqu'à son terme. La sortie de la console ressemblera à ceci : Thread-0 cède la place aux autres Thread-1 cède la place aux autres Thread-2 cède la place aux autres Thread-1 a terminé son exécution. Thread-0 a terminé son exécution. Thread-2 a terminé son exécution. Le planificateur de threads peut bien entendu exécuter les threads dans un ordre différent (par exemple, 2-1-0 au lieu de 0-1-2), mais le principe est le même.

Cela se produit avant les règles

La dernière chose que nous aborderons aujourd'hui concerne les principes « qui se produisent avant ». Comme vous le savez déjà, en Java, la majeure partie du travail d'allocation du temps et des ressources aux threads pour accomplir leurs tâches est effectuée par le planificateur de threads. De plus, vous avez vu plus d'une fois comment les threads sont exécutés dans un ordre arbitraire, et le plus souvent il est impossible de le prédire. Et en général, après la programmation « séquentielle » que nous faisions auparavant, le multithreading ressemble à une chose aléatoire. Comme vous l'avez déjà vu, la progression d'un programme multithread peut être contrôlée à l'aide de tout un ensemble de méthodes. Mais en plus de cela, dans le multithreading Java, il existe un autre « îlot de stabilité » - 4 règles appelées « arrive avant ». Littéralement de l'anglais, cela se traduit par « arrive avant » ou « arrive avant ». La signification de ces règles est assez simple à comprendre. Imaginez que nous ayons deux threads - Aet B. Chacun de ces threads peut effectuer des opérations 1et 2. Et quand dans chacune des règles on dit « A arrive avant B », cela signifie que toutes les modifications apportées par le thread Aavant l'opération 1et les changements que cette opération a entraînés sont visibles par le thread Bau moment où l'opération est effectuée 2et une fois l'opération effectuée. Chacune de ces règles garantit que lors de l'écriture d'un programme multithread, certains événements se produiront avant d'autres 100 % du temps, et que le thread Bau moment de l'opération 2sera toujours au courant des modifications qu'il Аa apportées au cours de l'opération. 1. Regardons-les.

Règle 1.

La libération d'un mutex se produit avant qu'un autre thread n'acquière le même moniteur. Eh bien, tout semble clair ici. Si le mutex d'un objet ou d'une classe est acquis par un thread, par exemple un thread А, un autre thread (thread B) ne peut pas l'acquérir en même temps. Vous devez attendre que le mutex soit libéré.

Règle 2.

La méthode Thread.start() se produit avant Thread.run() . Rien de compliqué non plus. Vous le savez déjà : pour que le code à l'intérieur de la méthode commence à s'exécuter run(), vous devez appeler la méthode sur le thread start(). C'est la sienne, et non la méthode elle-même run()! Cette règle garantit que Thread.start()les valeurs de toutes les variables définies avant l'exécution seront visibles dans la méthode qui a commencé l'exécution run().

Règle 3.

L'achèvement de la méthode run() se produit avant la sortie de la méthode join(). Revenons à nos deux flux - Аet B. Nous appelons la méthode join()de telle manière que le thread Bdoit attendre la fin Aavant de faire son travail. Cela signifie que la méthode run()de l'objet A fonctionnera définitivement jusqu'à la toute fin. Et tous les changements dans les données qui se produisent dans la méthode run()thread Aseront complètement visibles dans le thread Blorsqu'il attendra la fin Aet commencera à fonctionner lui-même.

Règle 4.

L'écriture dans une variable volatile a lieu avant la lecture à partir de la même variable. En utilisant le mot-clé volatile, nous obtiendrons en fait toujours la valeur actuelle. Même dans le cas de longet double, dont les problèmes ont été évoqués plus tôt. Comme vous l'avez déjà compris, les modifications apportées dans certains fils de discussion ne sont pas toujours visibles par les autres fils de discussion. Mais, bien sûr, il arrive très souvent que ce comportement du programme ne nous convienne pas. Disons que nous attribuons une valeur à une variable dans un threadA :
int z;.

z= 555;
Si notre thread Bdevait imprimer la valeur d'une variable zsur la console, il pourrait facilement imprimer 0 car il ne connaît pas la valeur qui lui est attribuée. Ainsi, la règle 4 nous le garantit : si vous déclarez une variable zcomme volatile, les modifications de ses valeurs dans un thread seront toujours visibles dans un autre thread. Si on ajoute le mot volatile au code précédent...
volatile int z;.

z= 555;
...la situation dans laquelle le flux Baffichera 0 sur la console est exclue. L'écriture dans des variables volatiles a lieu avant leur lecture.
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION