Le contenu principal de cet article est un point de connaissance commune dans l'interview de Java: les mots clés volatils. Cet article présente en détail tous les aspects des mots clés volatils. J'espère qu'après avoir lu cet article, vous pouvez parfaitement résoudre les problèmes connexes des mots clés volatils.
Dans les entretiens d'embauche liés à Java, de nombreux intervieweurs aiment examiner la compréhension de l'intervieweur de la concurrence de Java. En utilisant le mot-clé volatil comme petit point d'entrée, vous pouvez souvent demander au modèle de mémoire Java (JMM) et à certaines fonctionnalités de la programmation concurrente Java. En profondeur, vous pouvez également examiner les connaissances liées à la mise en œuvre et au système d'exploitation sous-jacentes. Prenons un processus d'entrevue hypothétique pour acquérir une compréhension approfondie du mot-clé volitile!
Autant que je comprends, les variables partagées modifiées par volatiles ont les deux caractéristiques suivantes:
1. Assurer la visibilité de la mémoire des différents threads à l'opération variable;
2. Interdire la réorganisation de la commande
C'est beaucoup à dire, donc je vais commencer par le modèle de mémoire Java. La spécification Java Virtual Machine tente de définir un modèle de mémoire Java (JMM) pour bloquer les différences d'accès à la mémoire entre diverses systèmes matériels et d'exploitation, afin que les programmes Java puissent obtenir des effets d'accès à la mémoire cohérents sur diverses plates-formes. Autrement dit, puisque le CPU exécute les instructions très rapidement, la vitesse d'accès à la mémoire est beaucoup plus lente et la différence n'est pas un ordre de grandeur, les grands gars qui travaillent sur le processeur ont ajouté plusieurs couches de cache au CPU. Dans le modèle de mémoire Java, l'optimisation ci-dessus est à nouveau abstraite. JMM stipule que toutes les variables sont en mémoire principale, similaires à la mémoire ordinaire mentionnée ci-dessus, et chaque thread contient sa propre mémoire de travail. Il peut être considéré comme un registre ou un cache sur le CPU pour une compréhension facile. Par conséquent, les opérations de thread sont principalement basées sur la mémoire de travail. Ils ne peuvent accéder qu'à leur propre mémoire de travail, et ils doivent synchroniser la valeur à la mémoire principale avant et après le travail. Je ne sais même pas ce que j'ai dit, prenez un morceau de papier à dessiner:
Lors de l'exécution d'un thread, la valeur de la variable sera d'abord lue à partir de la mémoire principale, puis chargée à la copie dans la mémoire de travail, puis la transmet au processeur pour exécution. Une fois l'exécution terminée, la copie dans la mémoire de travail se verra attribuer une valeur, puis la valeur dans la mémoire de travail sera reversée à la mémoire principale, et la valeur dans la mémoire principale sera mise à jour. Bien que l'utilisation de la mémoire de travail et de la mémoire principale soit plus rapide, elle apporte également quelques problèmes. Par exemple, regardez l'exemple suivant:
i = i + 1;
En supposant que la valeur initiale de I est 0, lorsqu'un seul thread l'exécute, le résultat obtiendra certainement 1. Lorsque deux threads s'exécuteront, le résultat obtiendra-t-il 2? Ce n'est pas nécessairement le cas. Cela peut être le cas:
Thread 1: Charge I à partir de la mémoire principale // i = 0 i + 1 // i = 1 Thread 2: Charge I de la mémoire principale // Parce que le thread 1 n'a pas écrit la valeur de I Retour à la mémoire principale, I est toujours 0 i + 1 // i = 1 Thread 1: Enregistrer I à la mémoire principale Thread 2: Enregistrer I à la mémoire principale
Si deux threads suivent le processus d'exécution ci-dessus, la dernière valeur de i est en fait 1. Si la dernière écriture est lente, et vous pouvez relire la valeur de I, il peut être 0, ce qui est le problème d'incohérence du cache. Ce qui suit est de mentionner la question que vous venez de poser. JMM est principalement établi sur la façon de gérer les trois caractéristiques de l'atomicité, de la visibilité et de l'ordre dans le processus de concurrence. En résolvant ces trois problèmes, le problème de l'incohérence du cache peut être résolu. Et le volatile est lié à la visibilité et à l'ordre.
1. Atomicité: En Java, les opérations de lecture et d'affectation des types de données de base sont des opérations atomiques. Les opérations dits atomiques signifient que ces opérations sont sans interruption et doivent être achevées pendant une certaine période, ou elles ne seront pas exécutées. Par exemple:
i = 2; j = i; i ++; i = i + 1;
Parmi les quatre opérations ci-dessus, i = 2 est une opération de lecture, qui doit être une opération atomique. J = Je pense que c'est une opération atomique. En fait, il est divisé en deux étapes. L'une consiste à lire la valeur de i, puis à attribuer la valeur à j. Il s'agit d'une opération en 2 étapes. Il ne peut pas être appelé une opération atomique. i ++ et i = i + 1 sont en fait équivalents. Lisez la valeur de i, ajoutez 1 et réécrivez-la à la mémoire principale. C'est une opération en 3 étapes. Par conséquent, dans l'exemple ci-dessus, la dernière valeur peut avoir de nombreuses situations car elle ne peut pas satisfaire l'atomicité. De cette façon, il n'y a qu'une lecture simple. L'attribution est une opération atomique ou une seule affectation numérique. Si vous utilisez des variables, il existe une opération supplémentaire pour lire la valeur variable. Une exception est que la spécification de la machine virtuelle permet de traiter les types de données 64 bits (longs et doubles) en 2 opérations, mais la dernière implémentation JDK implémente toujours les opérations atomiques. JMM implémente uniquement l'atomicité de base. Des opérations comme l'I ++ ci-dessus doivent être synchronisées et verrouiller pour assurer l'atomicité de l'ensemble du code. Avant que le thread ne libère le verrou, il brossera inévitablement la valeur de I Retour à la mémoire principale. 2. Visibilité: En parlant de visibilité, Java utilise volatile pour fournir une visibilité. Lorsqu'une variable est modifiée par volatile, la modification à elle sera immédiatement actualisée à la mémoire principale. Lorsque d'autres threads doivent lire la variable, la nouvelle valeur sera lue en mémoire. Ce n'est pas garanti par les variables ordinaires. En fait, le synchronisé et le verrouillage peuvent également assurer la visibilité. Avant que le thread ne libère le verrou, il rincera toutes les valeurs de variable partagée dans la mémoire principale, mais synchronisé et verrouillage sont plus chers. 3. La commande de JMM permet au compilateur et au processeur de réorganiser les instructions, mais stipule la sémantique tel qu'IF, c'est-à-dire, quelle que soit la réorganisation, le résultat d'exécution du programme ne peut pas être modifié. Par exemple, le segment du programme suivant:
double pi = 3,14; // adouble r = 1; // bdouble s = pi * r * r; // c
L'instruction ci-dessus peut être exécutée en a-> b-> c, le résultat étant de 3,14, mais il peut également être exécuté dans l'ordre de B-> a-> c. Parce que A et B sont deux déclarations indépendantes, tandis que C dépend de A et B, A et B peuvent être réorganisés, mais C ne peut pas être classé en premier dans A et B. JMM garantit que la réorganisation n'affectera pas l'exécution d'un seul fil, mais les problèmes sont sujets à se produire dans le multi-thread. Par exemple, un code comme ceci:
int a = 0; bool flag = false; public void write () {a = 2; // 1 drapeau = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Si deux threads exécutent le segment de code ci-dessus, le thread 1 exécute d'abord l'écriture, puis le thread 2 exécute ensuite Multiply, la valeur de RET doit-elle être 4? Le résultat n'est pas nécessairement:
Comme le montre la figure, 1 et 2 dans la méthode d'écriture sont réorganisés. Le thread 1 attribue d'abord l'indicateur à true, puis l'exécute au thread 2, RET calcule directement le résultat, puis au thread 1. À l'heure actuelle, A est affecté à 2, ce qui est évidemment une étape plus tard. Pour le moment, vous pouvez ajouter le mot-clé volatil au drapeau, interdire la réorganisation, ce qui peut garantir l'ordre du programme, et vous pouvez également utiliser des poids lourds synchronisés et verrouiller pour assurer l'ordre. Ils peuvent s'assurer que le code de cette zone est exécuté à la fois. De plus, JMM a un certain ordre inné, c'est-à-dire l'ordre qui peut être garanti sans aucun moyen, ce qui est généralement appelé le principe en passant avant. << JSR-133: modèle de mémoire Java et spécification de thread >> Définit les règles suivantes des règles: 1. Règles de séquence de programme: Pour chaque opération dans un thread, cela est utilisé pour toute opération ultérieure dans le thread 2. Domaine volatile 4. Transitivité: Si A se produit avant B et B se produit avant C, alors un se passer avant C 5.start () Règles: Si le thread A effectue une opération threadb_start () (Démarrer le thread b) du thread a Retour avec succès de l'opération threadb.join () dans le thread A. 7. Principe d'interruption (): l'appel à la méthode d'interruption () du thread se produit en premier lorsque l'événement d'interruption est détecté par le code de thread interrompu. Vous pouvez utiliser la méthode thread.interrupted () pour détecter s'il y a une interruption. 8. Finalize () Principe: L'achèvement d'initialisation d'un objet se produit en premier lorsque la méthode finalisée () commence. La première règle de la règle de séquence de programme indique que dans un fil, toutes les opérations sont en séquence, mais en JMM, tant que le résultat de l'exécution est le même, la réorganisation est autorisée. L'objectif de se passer avant ici est également l'exactitude du résultat d'exécution à un seul coup, mais il ne peut pas être garanti que la même chose est vraie pour le multi-threading. Règle 2 Les règles du moniteur sont en fait faciles à comprendre. Avant d'ajouter le verrou, vous ne pouvez continuer à ajouter la serrure. La règle 3 s'applique au volatile en question. Si un thread écrit d'abord une variable et qu'un autre thread le lit, l'opération d'écriture doit être avant l'opération de lecture. La quatrième règle est le transittivité des arrivants avant. Je n'entrerai pas dans les détails sur les quelques suivants.
Ensuite, nous devons les règles de variables volatiles réaménagées: écrivez un domaine volatil, en passant avant de lire ce domaine volatil plus tard. Laissez-moi retirer ça. En fait, si une variable est déclarée volatile, alors lorsque je lis la variable, je peux toujours lire sa dernière valeur. Ici, la dernière valeur signifie que peu importe quel autre thread écrit la variable, il sera immédiatement mis à jour vers la mémoire principale. Je peux également lire la valeur nouvellement écrite de la mémoire principale. En d'autres termes, le mot-clé volatil peut assurer la visibilité et l'ordre. Prenons le code ci-dessus comme exemple:
int a = 0; bool flag = false; public void write () {a = 2; // 1 drapeau = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Ce code n'est pas seulement troublé par la réorganisation, même si 1 et 2 ne sont pas réorganisés. 3 ne sera pas exécuté si bien non plus. Supposons que le thread 1 exécute d'abord l'opération d'écriture et que le thread 2 effectue ensuite l'opération de multiplication. Étant donné que le thread 1 attribue l'indicateur à 1 dans la mémoire de travail, il peut ne pas être réécrit immédiatement à la mémoire principale. Par conséquent, lorsque le thread 2 s'exécute, Multiply lit la valeur de l'indicateur de la mémoire principale, qui peut toujours être fausse, de sorte que les instructions entre parenthèses ne seront pas exécutées. S'il est changé pour ce qui suit:
int a = 0; boool volatile drapeau = false; public void write () {a = 2; // 1 drapeau = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Puis Thread 1 exécute d'abord l'écriture et le thread 2 exécute puis exécute Multiply. Selon le principe de Conting-Be avant, ce processus satisfera les trois types de règles suivants: Règles de l'ordre du programme: 1 arrive avant 2; 3 arrive avant 4; (Volatile restreint la réorganisation de l'instruction, donc 1 est exécuté avant 2) Règles volatiles: 2 arrive-avant 3 Règles transitives: 1 Avant 4 Lors de l'écriture d'une variable volatile, JMM rincera la variable partagée dans la mémoire locale correspondant au thread à la mémoire principale. Lors de la lecture d'une variable volatile, JMM définira la mémoire locale correspondant au thread pour invalider, et le thread lira la variable partagée à partir de la mémoire principale.
Tout d'abord, ma réponse est que l'atomicité ne peut être garantie. Si elle est garantie, ce n'est que l'atomicité pour la lecture / l'écriture d'une seule variable volatile, mais il n'y a rien à voir avec les opérations composées comme Volatile ++, comme l'exemple suivant:
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(); } while (thread.activeCount ()> 1) // Assurez-vous que les threads précédents ont terminé thread.yield (); System.out.println (test.inc); }Logiquement parlant, le résultat est de 10 000, mais il est probablement d'une valeur inférieure à 10 000 lors de l'exécution. Certaines personnes peuvent dire que Volatile ne garantit pas la visibilité. Un thread devrait voir les modifications à Inc par Inc, et l'autre fil doit le voir immédiatement! Mais l'opération Inc ++ ici est une opération composite, y compris la lecture de la valeur de Inc, l'augmentation par elle-même, puis la remettre à la mémoire principale. Supposons que le thread A lit la valeur d'Inc à 10, et il est bloqué à l'heure actuelle car la variable n'est pas modifiée et la règle volatile ne peut pas être déclenchée. Le thread B lit également la valeur de Inc pour le moment. La valeur de Inc dans la mémoire principale est toujours 10, et elle sera automatiquement augmentée, puis elle sera écrite dans la mémoire principale, qui est 11. À l'heure actuelle, c'est le tour du thread A pour s'exécuter. Étant donné que 10 est enregistré dans la mémoire de travail, il continue de s'accroître et réécrit à la mémoire principale. 11 est réécrit à nouveau. Ainsi, bien que les deux threads aient exécuté augmentent deux fois (), ils n'ont ajouté qu'une seule fois. Certaines personnes disent: volatile n'invalide-t-elle pas la ligne de cache? Cependant, avant que le thread A lit le thread B et effectue des opérations, la valeur inc n'est pas modifiée, donc lorsque le thread B se lit, il lit toujours 10. Certaines personnes disent également que si le thread B écrit 11 à la mémoire principale, ne définira pas la ligne de cache de Thread A pour invalider? Cependant, le thread A a déjà fait l'opération de lecture. Ce n'est que lorsque l'opération de lecture est terminée et que la ligne de cache est non valide qu'elle lira la valeur de mémoire principale. Par conséquent, le thread A ne peut que continuer à faire de l'auto-incitation. Pour résumer, dans ce type d'opération composite, la fonction atomique ne peut pas être maintenue. Cependant, dans l'exemple ci-dessus de la définition de la valeur de l'indicateur, car le fonctionnement de lecture / écriture des drapeaux est en une seule étape, il peut toujours assurer l'atomicité. Pour assurer l'atomicité, nous ne pouvons utiliser que des classes d'opération atomique synchronisées, verrouillables et atomiques sous des paquets simultanés, c'est-à-dire l'auto-incrément (ajouter 1 opération), l'auto-décoricité (réduire 1 opération), l'opération d'addition (ajouter un nombre) et l'opération de soustraction (soustraire un numéro) des types de données de base pour garantir que ces opérations sont des opérations.
Si vous générez du code d'assemblage avec le mot clé volatil et le code sans le mot clé volatil, vous constaterez que le code avec le mot clé volatil aura une instruction de préfixe de verrouillage supplémentaire. L'instruction de préfixe de verrouillage est en fait équivalente à une barrière de mémoire. La barrière de mémoire fournit les fonctions suivantes: 1. Lors de la réorganisation, les instructions suivantes ne peuvent pas être réorganisées à l'emplacement avant la barrière de la mémoire 2. Faire le cache de ce processeur écrit à la mémoire ** ** 3. L'action d'écriture provoquera également une autre valeur CPU ou d'autres noyaux.
1. Marque de quantité de statut, tout comme le drapeau ci-dessus, je le mentionnerai à nouveau:
int a = 0; boool volatile drapeau = false; public void write () {a = 2; // 1 drapeau = true; // 2} public void multiply () {if (flag) {// 3 int ret = a * a; // 4}}Cette opération de lecture et d'écriture en variables, marquée comme volatile, peut garantir que les modifications sont immédiatement visibles par le thread. Par rapport à la synchronisation, le verrou a une certaine amélioration de l'efficacité. 2. Implémentation du mode Singleton, serrure à double vérification typique (DCL)
Class Singleton {private volatile static singleton instance = null; private singleton () {} public static singleton getInstance () {if (instance == null) {synchronisé (singleton.class) {if (instance == null) instance = new singleton (); }} return instance; }}Il s'agit d'un motif de singleton paresseux, les objets sont créés uniquement lorsqu'ils sont utilisés, et pour éviter de réorganiser les instructions pour les opérations d'initialisation, volatile est ajoutée à l'instance.
Ce qui précède est l'intégralité du contenu de cet article sur l'explication des mots clés volatils que les intervieweurs de Java aiment demander en détail. J'espère que ce sera utile à tout le monde. Les amis intéressés peuvent continuer à se référer à d'autres sujets connexes sur ce site. S'il y a des lacunes, veuillez laisser un message pour le signaler. Merci vos amis pour votre soutien pour ce site!