Série de programmes concurrents Java [inachevé]:
• Programmation de concurrence Java: théorie centrale
• Programmation simultanée Java: synchronisée et ses principes de mise en œuvre
• Programmation concurrente Java: optimisation sous-jacente synchronisée (verrouillage léger, verrouillage biaisé)
• Programmation concurrente Java: collaboration entre les threads (attendre / notifier / dormir / rendement / join)
• Programmation concurrente Java: l'utilisation de volatils et de ses principes
1. Le rôle de volatile
Dans l'article "Java Connurrence Programming: Core Theory", nous avons mentionné les problèmes de visibilité, d'ordre et d'atomicité. Habituellement, nous pouvons résoudre ces problèmes via le mot-clé synchronisé. Cependant, si vous comprenez le principe de synchronisé, vous devez savoir que Synchronisé est une opération relativement lourde et a un impact relativement important sur les performances du système. Par conséquent, s'il existe d'autres solutions, nous évitons généralement d'utiliser synchronisé pour résoudre le problème. Le mot-clé volatil est une autre solution fournie en Java pour résoudre les problèmes de visibilité et d'ordre. En ce qui concerne l'atomicité, c'est aussi un point que tout le monde est sujet à des malentendus: une seule opération de lecture / écriture de variables volatiles peut assurer l'atomicité, comme les variables longues et doubles, mais elle ne peut garantir l'atomicité des opérations I ++, car, en substance, I ++ est des opérations de lecture et d'écriture deux fois.
2. Utilisation de volatile
En ce qui concerne l'utilisation de volatils, nous pouvons utiliser plusieurs exemples pour illustrer son utilisation et ses scénarios.
1. Empêcher la réorganisation
Analysons le problème de réorganisation de l'un des exemples les plus classiques. Tout le monde doit être familiarisé avec la mise en œuvre du modèle Singleton, et dans un environnement simultané, nous pouvons généralement utiliser la méthode de verrouillage à double vérification (DCL) pour l'implémenter. Le code source est le suivant:
Package com.paddx.test.concurrent; public class Singleton {public statile volatile singleton singleton; / ** * Le constructeur est privé, interdisant l'instanciation externe * / private singleton () {}; public static singleton getInstance () {if (singleton == null) {synchronisé (singleton) {if (singleton == null) {singleton = new Singleton (); }} return singleton; }}Analyons maintenant pourquoi nous devons ajouter le mot-clé volatil entre le singleton variable. Pour comprendre ce problème, vous devez d'abord comprendre le processus de construction des objets. L'instanciation d'un objet peut en fait être divisé en trois étapes:
(1) allouer l'espace mémoire.
(2) Initialiser l'objet.
(3) Attribuez l'adresse de l'espace mémoire à la référence correspondante.
Cependant, comme le système d'exploitation peut réorganiser les instructions, le processus ci-dessus peut également devenir le processus suivant:
(1) allouer l'espace mémoire.
(2) Attribuez l'adresse de l'espace mémoire à la référence correspondante.
(3) Initialiser l'objet
Si ce processus est le processus, une référence d'objet non initialisée peut être exposée dans un environnement multi-thread, ce qui entraîne des résultats imprévisibles. Par conséquent, pour éviter la réorganisation de ce processus, nous devons définir la variable sur une variable de type volatile.
2. Réaliser une visibilité
Le problème de visibilité se réfère principalement à un thread modifiant la valeur de variable partagée, tandis que l'autre thread ne peut pas le voir. La raison principale du problème de visibilité est que chaque thread a sa propre zone de cache - mémoire de travail de thread. Le mot-clé volatil peut résoudre efficacement ce problème. Examinons les exemples suivants pour connaître sa fonction:
package com.paddx.test.concurrent; public class volatileTest {int a = 1; int b = 2; public void change () {a = 3; b = a; } public void print () {System.out.println ("b =" + b + "; a =" + a); } public static void main (string [] args) {while (true) {final volatestest test = new volatileTest (); nouveau thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {e.printStackTrace ();} test.change ();}}). start (); nouveau thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {e.printStackTrace ();} test.print ();}}). start (); }}}Intuitivement parlant, il n'y a que deux résultats possibles pour ce code: b = 3; a = 3 ou b = 2; a = 1. Cependant, exécutant le code ci-dessus (peut-être que cela prend un peu plus de temps), vous constaterez qu'en plus des deux résultats précédents, il y a aussi un troisième résultat:
...... b = 2; a = 1b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 1b = 3; a = 3b = 2; a = 1b = 3; a = 3b = 3; a = 3b = 3; a = 3 ...
Pourquoi un résultat tel que b = 3; a = 1 apparaît-il? Dans des circonstances normales, si vous exécutez d'abord la méthode de modification, puis exécutez la méthode d'impression, le résultat de sortie doit être b = 3; a = 3. Au contraire, si vous exécutez d'abord la méthode d'impression, puis exécutez la méthode de changement, le résultat doit être b = 2; a = 1. Alors, comment le résultat de b = 3; a = 1 sort-il? La raison en est que le premier thread modifie la valeur a = 3, mais est invisible au deuxième thread, donc ce résultat se produit. Si A et B sont modifiés en variables de type volatile et exécutés, le résultat de B = 3; A = 1 n'apparaîtra plus jamais.
3. Assurer l'atomicité
La question de l'atomicité a été expliquée ci-dessus. Volatile ne peut garantir l'atomicité que pour une lecture / écriture unique. Ce problème peut être décrit dans JLS:
17.7 Traitement non atomique du double et long pour les fins du modèle de mémoire de langage de programmation Java, une seule écriture à une valeur longue ou double non volatile est traitée comme deux écritures distinctes: une à chaque moitié de 32 bits. Cela peut entraîner une situation où un thread voit les 32 premiers bits d'une valeur 64 bits à partir d'une écriture, et les 32 seconds d'un autre écriture. Les écritures et les lectures de valeurs longues et doubles sont toujours atomiques. Les écritures et les lectures des références sont toujours atomiques, qu'elles soient mises en œuvre comme des valeurs 32 bits ou 64 bits. Certaines implémentations peuvent trouver pratique de diviser une seule action d'écriture sur une valeur de 64 bits ou une double valeur en deux actions d'écriture sur des valeurs 32 bits adjacentes. Par souci d'efficacité, ce comportement est spécifique à la mise en œuvre; Une implémentation de la machine virtuelle Java est libre d'effectuer des écritures à des valeurs longues et doubles atomiquement ou en deux parties. Les implémentations de la machine virtuelle Java sont encouragées à éviter de diviser les valeurs 64 bits dans la mesure du possible. Les programmeurs sont encouragés à déclarer les valeurs partagées 64 bits comme volatiles ou à synchroniser correctement leurs programmes pour éviter d'éventuelles combplications.
Le contenu de ce passage est à peu près similaire à ce que j'ai décrit précédemment. Étant donné que les opérations des deux types de données longs et doubles peuvent être divisées en deux parties: 32 bits élevés et 32 bits, des types longs ou doubles ordinaires peuvent ne pas être atomiques. Par conséquent, tout le monde est encouragé à définir les variables longues et doubles partagées sur des types volatils, ce qui peut garantir que les opérations de lecture / écriture unique de long et doubles sont atomiques dans tous les cas.
Il y a un problème que les variables volatiles garantissent l'atomicité, ce qui est facilement mal compris. Nous allons maintenant démontrer ce problème à travers le programme suivant:
package com.paddx.test.concurrent; public class volatileTest01 {volatile int i; public void adddi () {i ++; } public static void main (String [] args) lève InterruptedException {final volatileTest01 test01 = new VolateTest01 (); pour (int n = 0; n <1000; n ++) {new Thread (new Runnable () {@Override public void run () {try {thread.sleep (10);} catch (interruptedException e) {e.printStackTrace ();} test01.addi ();}}). start (); } Thread.sleep (10000); // attendez 10 secondes pour garantir que l'exécution du programme ci-dessus est terminée System.out.println (test01.i); }}Vous pouvez croire à tort qu'après avoir ajouté le mot-clé volatile à la variable I, ce programme est fileté. Vous pouvez essayer d'exécuter le programme ci-dessus. Voici les résultats de ma course locale:
Peut-être que tout le monde gère les résultats différemment. Cependant, il convient de voir que le volatile ne peut garantir l'atomicité (sinon le résultat devrait être de 1000). La raison est également très simple. I ++ est en fait une opération composite, y compris trois étapes:
(1) Lisez la valeur de i.
(2) Ajouter 1 à i.
(3) Écrivez la valeur de I à la mémoire.
Il n'y a aucune garantie que ces trois opérations sont atomiques. Nous pouvons assurer l'atomicité des opérations +1 via AtomicInteger ou synchronisée.
Remarque: La méthode thread.sleep () a été exécutée dans de nombreux endroits dans les sections de code ci-dessus, dans le but d'augmenter les chances de problèmes de concurrence et n'a aucun autre effet.
3. Le principe de volatile
Grâce aux exemples ci-dessus, nous devons essentiellement savoir ce qu'est volatile et comment l'utiliser. Voyons maintenant comment la couche sous-jacente de volatile est mise en œuvre.
1. Mise en œuvre de la visibilité:
Comme mentionné dans l'article précédent, le thread lui-même n'interagit pas directement avec les données de mémoire principale, mais complète les opérations correspondantes via la mémoire de travail du thread. C'est également la raison essentielle pour laquelle les données entre les threads sont invisibles. Par conséquent, pour atteindre la visibilité des variables volatiles, vous pouvez commencer directement à partir de cet aspect. Il existe deux principales différences entre les opérations d'écriture sur les variables volatiles et les variables ordinaires:
(1) Lors de la modification de la variable volatile, la valeur modifiée sera forcée de rafraîchir la mémoire principale.
(2) La modification de la variable volatile entraînera l'échec des valeurs de variable correspondantes dans la mémoire de travail des autres threads. Par conséquent, lorsque vous lisez à nouveau la valeur de cette variable, vous devez à nouveau relire la valeur dans la mémoire principale.
Grâce à ces deux opérations, le problème de visibilité des variables volatiles peut être résolu.
2. Implémentation ordonnée:
Avant d'expliquer ce problème, comprenons d'abord les règles qui se produisent avant Java. La définition des événements dans JSR 133 est la suivante:
Deux actions peuvent être commandées par une relation provenant. Si une action se produit avant une autre, la première est visible et commandée avant la seconde.
En termes de laïcs, en cas de bêti-avant, toutes les opérations a sont visibles à b. (Tout le monde doit s'en souvenir, car le mot qui se passe avant est facilement mal compris comme avant et après le temps). Jetons un coup d'œil à ce que les règles sont définies dans JSR 133:
• Chaque action dans un fil se produit avant chaque action ultérieure de ce fil. • Un déverrouillage sur un moniteur se produit avant chaque verrouillage suivant sur ce moniteur. • Une écriture dans un champ volatil se produit avant chaque lecture ultérieure de ce volatil. • Un appel à démarrer () sur un thread se produit avant toute action du thread démarré. • Toutes les actions dans un thread se produisent avant que tout autre thread ne revienne avec succès à partir d'un join () sur ce thread. • Si une action a se produit avant une action B, et B se produit avant une action C, alors A se produit avant c.
Traduit par:
• L'opération précédente se produit dans le même thread. (c'est-à-dire dans un seul thread, il est légal d'exécuter dans l'ordre du code. Cependant, le compilateur et le processeur peuvent réorganiser sans affecter l'exécution des résultats dans un seul environnement de thread. En d'autres termes, c'est que les règles ne peuvent garantir la réorganisation de la compilation et la réorganisation de l'enseignement).
• Déverrouiller le fonctionnement sur le moniteur qui se passe avant son opération de verrouillage ultérieure. (Règles synchronisées)
• Écrivez l'opération à des opérations de lecture ultérieures variables volatiles. (Règles volatiles)
• La méthode start () du thread se produit avant toutes les opérations ultérieures du thread. (Règle de démarrage du fil)
• Toutes les opérations du thread se produisent avant les autres threads appellent la jointure de ce thread et renvoient l'opération réussie.
• Si un cas avant B, b fait avant C, alors un peu avant C (transitif).
Ici, nous examinons principalement la troisième règle: les règles pour assurer l'ordre des variables volatiles. L'article "Java Connurrence Programming: Core Theory" a mentionné que la réorganisation est divisée en réorganisation du compilateur et réorganisation du processeur. Pour implémenter la sémantique de mémoire volatile, JMM restreint la réorganisation de ces deux types de variables volatiles. Ce qui suit est le tableau des règles de réorganisation spécifiées par JMM pour les variables volatiles:
| Peut réorganiser | 2e opération | |||
| 1ère opération | Charge normale Magasin normal | Charge volatile | Magasin volatil | |
| Charge normale Magasin normal | Non | |||
| Charge volatile | Non | Non | Non | |
| Magasin volatil | Non | Non | ||
3. Barrière de mémoire
Afin de mettre en œuvre une visibilité volatile et une sémantique de semence-befor. Le JVM sous-jacent se fait à travers quelque chose appelé une "barrière de mémoire". La barrière de mémoire, également connue sous le nom de clôture de mémoire, est un ensemble d'instructions de processeur utilisées pour implémenter des restrictions séquentielles sur les opérations de mémoire. Voici la barrière de mémoire requise pour terminer les règles ci-dessus:
| Barrières requises | 2e opération | |||
| 1ère opération | Charge normale | Magasin normal | Charge volatile | Magasin volatil |
| Charge normale | Standard | |||
| Magasin normal | Storestore | |||
| Charge volatile | Charge de chargement | Standard | Charge de chargement | Standard |
| Magasin volatil | Insérer | Storestore | ||
(1) barrière de charge de chargement
Ordre d'exécution: Load1 load `` Load2
Assurez-vous que les instructions de chargement et de chargement suivantes peuvent accéder aux données chargées par chargement1 avant de charger des données.
(2) Barrière Storestore
Ordre d'exécution: Store1 lot Storestore -> Store2
Assurez-vous que les données de l'opération STORE1 sont visibles pour d'autres processeurs avant que les instructions de magasin et de magasin suivantes ne soient exécutées.
(3) barrière de chargement
Ordre d'exécution: Load1 loadStore -> Store2
Assurez-vous qu'avant les instructions Store2 et Store suivantes, les données chargées par charge1 sont accessibles.
(4) Barrière de la verge
Ordre d'exécution: Store1 -> Storeload -> Load2
Assurez-vous qu'avant la lecture des instructions de chargement 2 et de chargement suivantes, les données de Store1 sont visibles pour d'autres processeurs.
Enfin, je peux utiliser un exemple pour illustrer comment la barrière de mémoire est insérée dans le JVM:
package com.paddx.test.concurrent; public class MemoryBarrier {int a, b; volatile int v, u; void f () {int i, j; i = a; j = b; i = v; // charge de charge j = u; // Loadstore a = i; b = j; // Storestore v = i; // Storestore U = J; // storeload i = u; // Loadload // LoadStore j = b; a = i; }}4. Résumé
Dans l'ensemble, la compréhension du volatile est encore relativement difficile. Si vous ne le comprenez pas en particulier, vous n'avez pas besoin de vous dépêcher. Il faut un processus pour le bien comprendre. Vous verrez également les scénarios d'utilisation de volatils à plusieurs reprises dans des articles suivants. Ici, j'ai une compréhension de base des connaissances de base du volatile et de l'original. D'une manière générale, le volatile est une optimisation dans la programmation simultanée, qui peut remplacer synchronisé dans certains scénarios. Cependant, le volatile ne peut pas complètement remplacer la position de synchronisée. Ce n'est que dans certains scénarios spéciaux que peut être appliqué. En général, les deux conditions suivantes doivent être remplies en même temps pour assurer la sécurité des filetages dans un environnement simultané:
(1) L'opération d'écriture en variables ne dépend pas de la valeur actuelle.
(2) Cette variable n'est pas incluse dans l'invariant avec d'autres variables.
L'article ci-dessus sur la programmation simultanée Java: l'utilisation de volatile et son analyse principale est tout le contenu que je partage avec vous. J'espère que vous pourrez vous faire référence et j'espère que vous pourrez soutenir Wulin.com plus.