Préface
Parfois, si vous utilisez la synchronisation juste pour lire et écrire un ou deux champs d'instance, cela semble trop cher. Le mot-clé volatil fournit un mécanisme sans serrure pour l'accès synchrone aux champs d'instance. Si un domaine est déclaré volatil, le compilateur et la machine virtuelle savent que le domaine peut être mis à jour simultanément par un autre thread. Avant de parler du mot-clé volatil, nous devons comprendre les concepts pertinents du modèle de mémoire et les trois caractéristiques de la programmation simultanée: atomicité, visibilité et ordre.
1. Modèle de mémoire Java avec atomicité, visibilité et ordonnance
Le modèle de mémoire Java stipule que toutes les variables existent dans la mémoire principale, et chaque thread a sa propre mémoire de travail. Toutes les opérations d'un thread sur une variable doivent être effectuées en mémoire de travail et ne peuvent pas fonctionner directement sur la mémoire principale. Et chaque thread ne peut pas accéder à la mémoire de travail des autres threads.
Dans Java, exécutez l'énoncé suivant:
int i = 3;
Le thread d'exécution doit d'abord attribuer la ligne de cache où la variable I est située dans son propre thread de travail, puis l'écrire dans la mémoire principale. Au lieu d'écrire directement la valeur 3 dans la mémoire principale.
Alors, quelles garanties le langage Java lui-même assure-t-il pour l'atomicité, la visibilité et l'ordre?
Atomicité
Les opérations de lecture et d'affectation des variables du type de données de base sont des opérations atomiques, c'est-à-dire que ces opérations ne peuvent pas être interrompues et exécutées ou non.
Jetons un coup d'œil au code suivant:
x = 10; // déclaration 1y = x; // instruction 2x ++; // instruction 3x = x + 1; // Déclaration 4
La seule déclaration 1 est une opération atomique, et aucune des trois autres déclarations n'est des opérations atomiques.
L'énoncé 2 contient en fait 2 opérations. Il doit d'abord lire la valeur de x, puis écrire la valeur de x à la mémoire de travail. Bien que les deux opérations de lecture de la valeur de X et d'écriture de la valeur de X à la mémoire de travail soient des opérations atomiques, ce ne sont pas des opérations atomiques ensemble.
De même, x ++ et x = x + 1 incluent 3 opérations: lire la valeur de x, effectuer le fonctionnement de l'ajout de 1 et écrire la nouvelle valeur.
En d'autres termes, seule une lecture et une affectation simples (et le nombre doit être attribué à une variable, et l'affectation mutuelle entre les variables n'est pas une opération atomique) est une opération atomique.
Il existe de nombreuses classes dans le package java.util.concurrent.atomique qui utilisent des instructions de niveau de machine très efficaces (plutôt que des verrous) pour assurer l'atomicité des autres opérations. Par exemple, la classe ATOMICInteger fournit des méthodes incrément et le gel et la diminution et le gel, qui augmentent respectivement et diminuent un entier de manière atomique. La classe AtomicInteger peut être utilisée en toute sécurité comme compteur partagé sans synchronisation.
De plus, ce package contient également des classes atomiques telles que Atomicboolan, Atomiclong et AtomiCreference pour les programmeurs système qui développent uniquement des outils simultanés, et les programmeurs d'application ne devraient pas utiliser ces classes.
Visibilité
La visibilité fait référence à la visibilité entre les fils et l'état modifié d'un thread est visible à un autre fil. C'est le résultat d'une modification de thread. Un autre fil sera vu immédiatement.
Lorsqu'une variable partagée est modifiée par volatile, elle garantit que la valeur modifiée sera mise à jour immédiatement dans la mémoire principale, il est donc visible pour d'autres threads. Lorsque d'autres threads doivent lire, il lira la nouvelle valeur en mémoire.
Cependant, les variables partagées ordinaires ne peuvent garantir la visibilité, car il est incertain lorsque la variable partagée normale est écrite dans la mémoire principale après sa modification. Lorsque d'autres fils le lisent, l'ancienne valeur d'origine peut toujours être dans la mémoire, donc la visibilité ne peut être garantie.
Ordonné
Dans le modèle de mémoire Java, les compilateurs et les processeurs sont autorisés à réorganiser les instructions, mais le processus de réorganisation n'affectera pas l'exécution des programmes unique, mais affectera l'exactitude de l'exécution simultanée multi-thread.
Le mot-clé volatil peut être utilisé pour assurer une certaine "ligne d'ordre". De plus, le synchronisé et le verrouillage peuvent être utilisés pour assurer la commande. De toute évidence, le synchronisé et le verrouillage garantissent qu'il existe un thread qui exécute le code de synchronisation à chaque instant, ce qui équivaut à laisser les threads exécuter le code de synchronisation en séquence, ce qui garantit naturellement l'ordre.
2. Mots-clés volatils
Une fois qu'une variable partagée (variables des membres de la classe, les variables de membres statiques de classe) est modifiée par volatile, il a deux couches de sémantique:
Regardons d'abord un morceau de code. Si le thread 1 est exécuté en premier et que le thread 2 est exécuté ultérieurement:
// Thread 1boolean stop = false; while (! stop) {DoSomething ();} // Thread 2Stop = true; De nombreuses personnes peuvent utiliser cette méthode de balisage lors de l'interruption de fils. Mais en fait, ce code fonctionnera-t-il complètement correctement? Le fil sera-t-il interrompu? Pas nécessairement. Peut-être que la plupart du temps, ce code peut interrompre les threads, mais il peut également ne pas interrompre le fil (bien que cette possibilité soit très petite, une fois que cela se produira, il provoquera une boucle morte).
Pourquoi est-il possible de provoquer l'interruption de défaillance du thread? Chaque thread a sa propre mémoire de travail pendant son processus d'exécution. Lorsque le thread 1 est en cours d'exécution, il copiera la valeur de la variable d'arrêt et le mettra dans sa propre mémoire de travail. Ensuite, lorsque le thread 2 modifie la valeur de la variable d'arrêt, mais n'a pas eu le temps de l'écrire dans la mémoire principale, le thread 2 va faire d'autres choses, puis le thread 1 ne connaît pas les modifications du thread 2 dans la variable d'arrêt, il continuera donc à boucler.
Mais après avoir modifié avec volatile, il devient différent:
Volatile garantit-il l'atomicité?
Nous savons que le mot-clé volatil garantit la visibilité des opérations, mais que volatile peut garantir que les opérations sur les variables sont atomiques?
Test de classe publique {public volatile int inc = 0; public void augmentation () {inc ++; } public static void main (String [] args) {final test test = new test (); for (int i = 0; i <10; i ++) {new Thread () {public void run () {for (int j = 0; j <1000; j ++) test.inCrease (); }; }.commencer(); } // Assurez-vous que les threads précédents ont terminé l'exécution tandis que (thread.activeCount ()> 1) thread.yield (); System.out.println (test.inc); }} Le résultat de ce code est incohérent à chaque fois qu'il fonctionne. C'est un nombre inférieur à 10 000. Comme mentionné précédemment, l'opération d'automobile n'est pas atomique. Il comprend la lecture de la valeur d'origine d'une variable, effectuant une opération supplémentaire 1 et écrivant dans la mémoire de travail. C'est-à-dire que les trois sous-opérations de l'opération d'auto-incitation peuvent être exécutées séparément.
Si la valeur de Variable Inc est 10 à un certain moment, le thread 1 effectue un opération d'auto-incidence sur la variable, le thread 1 lit d'abord la valeur d'origine de Variable Inc, puis le thread 1 est bloqué; Puis Thread 2 effectue un fonctionnement d'auto-incrémentation sur la variable, et Thread 2 lit également la valeur d'origine de Variable Inc. Étant donné que Thread 1 effectue une opération de lecture uniquement sur la variable Inc et ne modifie pas la variable, elle ne provoquera pas la ligne de cache de Cache Variable Inc dans le thread 2 invalide dans la mémoire de travail, donc le thread 2 ira directement à la mémoire principale pour lire la valeur de Inc. Lorsqu'il est constaté que la valeur de Inc est 10, alors effectue une augmentation de 1 et écrit 11 à la mémoire de travail, et l'écrit enfin dans la mémoire principale. Ensuite, le thread 1 effectue ensuite l'opération d'addition. Étant donné que la valeur de l'inc a été lue, notez que la valeur de Inc dans le thread 1 est toujours 10 pour le moment, donc après le thread 1 ajoute Inc, la valeur de Inc est 11, puis écrit 11 pour travailler la mémoire et l'écrit enfin dans la mémoire principale. Ensuite, après que les deux threads effectuent une opération d'auto-incrément, INC n'augmente que 1.
L'opération d'auto-inférences n'est pas une opération atomique, et volatile ne peut garantir qu'une opération sur une variable est atomique.
Le volatile peut-il assurer l'ordre?
Comme mentionné précédemment, le mot-clé volatil peut interdire la réorganisation des instructions, de sorte que volatile peut garantir l'ordre dans une certaine mesure.
Il y a deux significations interdites de réorganisation des mots clés volatils:
3. Utilisez correctement le mot clé volatile
Le mot-clé synchronisé empêche plusieurs threads d'exécuter un morceau de code en même temps, ce qui affectera grandement l'efficacité d'exécution du programme. Les performances du mot-clé volatil sont meilleures que la synchronisation dans certains cas. Cependant, il convient de noter que le mot clé volatil ne peut pas remplacer le mot clé synchronisé, car le mot clé volatil ne peut pas garantir l'atomicité de l'opération. D'une manière générale, les deux conditions suivantes doivent être remplies lors de l'utilisation du volatile:
La première condition est qu'il ne peut pas être des opérations telles que l'auto-augmentation et l'auto-décoricité. Comme mentionné ci-dessus, Volatile ne garantit pas l'atomicité.
En donnons un exemple de cela. Il contient un invariant: la limite inférieure est toujours inférieure ou égale à la borne supérieure.
classe publique NumberRange {privé volatile int inférieur, supérieur; public int getlower () {return inférieur; } public int getUpper () {return upper; } public void setLower (int value) {if (value> upper) lance un nouveau IllégalArgumentException (...); inférieur = valeur; } public void setupper (int value) {if (valeur <inférieur) lance un nouveau IllégalArgumentException (...); Upper = valeur; }} De cette façon, il limite les variables d'état dans la portée, la définition des champs inférieurs et supérieurs en tant que types volatils n'implémente pas pleinement la sécurité du thread de la classe, et nécessite donc toujours une synchronisation. Sinon, si deux threads exécutent SetLower et Settupper avec des valeurs incohérentes en même temps, la plage sera incohérente. Par exemple, si l'état initial est (0, 5), en même temps, le thread A appelle SetLower (4) et que le thread B appelle SETUPPER (3), il est évident que les valeurs stockées dans le cross-stockage par ces deux opérations ne remplissent pas les conditions, alors les deux threads passeront évidemment pour protéger l'invaariant, de sorte que la dernière valeur de plage est (4, 3), ce qui est évidemment erroné.
En fait, il s'agit de garantir que l'atomicité de l'opération utilise volatile. Il y a deux scénarios principaux pour l'utilisation du volatile:
Drapeaux d'état
Boolean Volatile ShutdownReted; ... public void shutdown () {shutdownRequeted = true; } public void Dowork () {while (! shutdownRequeted) {// faire des trucs}}Il est probable que la méthode d'arrêt () sera appelée depuis l'extérieur de la boucle - c'est-à-dire dans un autre thread - donc une sorte de synchronisation doit être effectuée pour garantir la correction de la visibilité de la variable de fermeture. Cependant, l'écriture d'une boucle avec des blocs synchronisés est beaucoup plus gênante que l'écriture avec des indicateurs de statut volatils. Étant donné que le volatil simplifie le codage et les indicateurs de statut ne dépendent d'un autre état du programme, il est très adapté à la volatile ici.
Mode à double vérification (DCL)
classe publique Singleton {Instance singleton statique volatile privée = null; public static singleton getInstance () {if (instance == null) {synchronisé (this) {if (instance == null) {instance = new singleton (); }} return instance; }} L'utilisation de volatile ici affectera plus ou moins les performances, mais compte tenu de l'exactitude du programme, il vaut toujours la peine de sacrifier cette performance.
L'avantage de DCL est qu'il a une utilisation élevée des ressources. Les objets singleton ne sont instanciés que lorsque la GetInstance est exécutée pour la première fois, ce qui est très efficace. L'inconvénient est que la réaction est légèrement plus lente lors du chargement pour la première fois, et il y a certains défauts dans des environnements de concurrence élevés, bien que la probabilité d'occurrence soit très faible.
Bien que DCL résout les problèmes de consommation de ressources, de synchronisation inutile, de sécurité des fils, etc., dans une certaine mesure, il a toujours des problèmes de défaillance dans certains cas, c'est-à-dire une défaillance de DCL. Dans le livre "Java Concurrency Programming Practice", il recommande d'utiliser le code suivant (modèle de singleton de classe interne statique) pour remplacer DCL:
classe publique Singleton {private singleton () {} public static singleton getInstance () {return singletonholder.sinstance; } classe statique privée singletonholder {private statique final singleton sinstance = new singleton (); }} À propos des doubles chèques, vous pouvez voir
4. Résumé
Par rapport aux verrous, la variable volatile est un mécanisme de synchronisation très simple mais en même temps très fragile qui offrira dans certains cas des performances et de meilleures performances que les verrous. Si vous suivez strictement les conditions d'utilisation de volatiles qui sont vraiment indépendantes des autres variables et de leurs propres valeurs précédentes, vous pouvez utiliser volatile au lieu de synchronisé pour simplifier le code dans certains cas. Cependant, le code utilisant volatile est souvent plus sujet aux erreurs que le code à l'aide de verrous. Cet article présente deux cas d'utilisation les plus courants où volatile peut être utilisé à la place de synchronisé. Dans d'autres cas, nous ferions mieux d'utiliser synchronisé.
Ce qui précède concerne cet article, j'espère qu'il sera utile à l'apprentissage de tout le monde.