Après avoir travaillé pendant 5 ans, je ne connais pas le mot clé volatile?

"Après avoir travaillé pendant 5 ans, je ne connais même pas le mot clé volatile!"

Après avoir écouté l'architecte qui vient de terminer l'entretien, plusieurs autres collègues ont également participé à ce temps.

On dit que les entretiens nationaux consistent à «interroger les porte-avions et à visser les vis au travail». Parfois, vous serez PASSÉ à cause d'un problème.

Combien de temps travaillez-vous? Connaissez-vous le mot-clé volatile?

Aujourd'hui, apprenons ensemble le mot-clé volatile et soyons un ouvrier visseux capable de construire un porte-avions en entretien!

Introduction à volatile +

volatil

La définition de volatile dans la troisième édition de la spécification du langage Java est la suivante:

Le langage de programmation Java permet aux threads d'accéder aux variables partagées. Afin de s'assurer que les variables partagées peuvent être mises à jour avec précision et cohérence, les threads doivent s'assurer que cette variable est acquise individuellement via un verrou exclusif.

Le langage Java fournit volatile, ce qui est plus pratique que les verrous dans certains cas.

Si un champ est déclaré volatil, le modèle de mémoire de thread Java garantit que tous les threads voient la valeur de cette variable cohérente.

Sémantique

Une fois qu'une variable partagée (variable de membre de classe, variable de membre statique de classe) est modifiée par volatile, alors elle a deux couches de sémantique:

  1. Cela garantit la visibilité des différents threads opérant sur cette variable, c'est-à-dire que si un thread modifie la valeur d'une variable, la nouvelle valeur est immédiatement visible par les autres threads.

  2. La réorganisation des instructions est interdite.
  • Remarque

Si la variable finale est également déclarée comme volatile, il s'agit d'une erreur de compilation.

ps: L'un signifie que les changements sont visibles, et l'autre signifie qu'ils ne changent jamais. Le feu naturel et l'eau sont incompatibles.

Introduction du problème

  • Error.java
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}

//线程2
stop = true;

Ce code est un morceau de code typique, et de nombreuses personnes peuvent utiliser cette méthode de marquage lors de l'interruption d'un thread.

analyse du problème

Mais en fait, ce code fonctionnera-t-il correctement? Le fil sera-t-il interrompu?

Pas nécessairement. Peut-être que la plupart du temps, ce code peut interrompre le thread, mais il peut aussi le faire échouer à interrompre le thread (bien que cette possibilité soit faible, tant que cela se produit, cela provoquera une boucle infinie).

Ce qui suit explique pourquoi ce code peut provoquer l'échec de l'interruption du thread.

Comme expliqué précédemment, chaque thread a sa propre mémoire de travail pendant le fonctionnement, donc lorsque le thread 1 est en cours d'exécution, il copiera la valeur de la variable d'arrêt et la placera dans sa propre mémoire de travail.

Ensuite, lorsque le thread 2 change la valeur de la variable d'arrêt, mais qu'il n'a pas eu le temps de l'écrire dans la mémoire principale, le thread 2 se déplace pour faire autre chose,

Ensuite, le thread 1 ne connaît pas les modifications apportées par le thread 2 à la variable d'arrêt, il continuera donc à boucler.

Utilisez volatile

Premièrement: l'utilisation du mot-clé volatile forcera l'écriture immédiate de la valeur modifiée dans la mémoire principale;

Deuxièmement: en utilisant le mot-clé volatile, lorsque le thread 2 est modifié, la ligne de cache de la variable de cache stop dans la mémoire de travail du thread 1 sera invalide (reflétée sur la couche matérielle, c'est la ligne de cache correspondante dans le cache L1 ou L2 du CPU invalide);

Troisièmement: étant donné que la ligne de cache de la variable de tampon stop dans la mémoire de travail du thread 1 n'est pas valide, le thread 1 ira dans la mémoire principale pour lire à nouveau la valeur de la variable stop.

Ensuite, lorsque le thread 2 modifie la valeur d'arrêt (bien sûr, cela comprend deux opérations, la modification de la valeur dans la mémoire de travail du thread 2, puis l'écriture de la valeur modifiée dans la mémoire),
la ligne de cache de la variable stop sera mise en cache dans la mémoire de travail du thread 1 Non valide, puis lorsque le thread 1 lit, il
constate que sa ligne de cache n'est pas valide. Il attend que l'adresse de la mémoire principale correspondant à la ligne de cache soit mise à jour, puis accède à la mémoire principale correspondante pour lire la dernière valeur.

Ensuite, le thread 1 lit la dernière valeur correcte.

Le volatile garantit-il l'atomicité?

De ce qui précède, nous savons que le mot-clé volatile garantit la visibilité de l'opération, mais volatile peut-il garantir que le fonctionnement de la variable est atomique?

Introduction du problème

public class VolatileAtomicTest {

    public volatile int inc = 0;

    public void increase() {
        inc++;
    }

    public static void main(String[] args) {
        final VolatileAtomicTest test = new VolatileAtomicTest();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    test.increase();
                }
            }).start();
        }

        //保证前面的线程都执行完
        while (Thread.activeCount() > 1) {
            Thread.yield();
        }
        System.out.println(test.inc);
    }
}
  • Quel est le résultat du calcul?

Vous pensez peut-être qu'il est de 10 000, mais il est en fait plus petit que ce nombre.

la raison

Peut-être que certains amis auront des questions. Ce n'est pas juste. Ce qui précède est d'auto-incrémenter la variable inc. Puisque volatile garantit la visibilité,
puis après l'incrémentation automatique de inc dans chaque thread, il peut être vu dans d'autres threads. La valeur modifiée, donc 10 threads ont effectué 1000 opérations respectivement, alors la valeur finale inc doit être 1000 * 10 = 10000.

Il y a un malentendu ici: le mot-clé volatile peut garantir que la visibilité n'est pas mauvaise, mais le programme ci-dessus est faux en ce qu'il ne garantit pas l'atomicité.

La visibilité ne peut garantir que la dernière valeur est lue à chaque fois, mais volatile ne peut garantir l'atomicité des opérations sur les variables.

  • Solution

Utiliser Lock synchronized ou AtomicInteger

La volatilité peut-elle garantir l'ordre?

Le mot clé volatile interdit la réorganisation des instructions a deux significations:

  1. Lorsque le programme exécute l'opération de lecture ou d'écriture de la variable volatile, toutes les modifications de l'opération précédente doivent avoir été effectuées et le résultat a été visible pour l'opération suivante; l'opération suivante ne doit pas avoir été effectuée;

  2. Lors de l'optimisation des instructions, vous ne pouvez pas placer les instructions qui accèdent aux variables volatiles derrière elles pour l'exécution, et vous ne pouvez pas placer les instructions qui suivent les variables volatiles avant elles pour exécution.

Exemple

  • Exemple un
//x、y为非volatile变量
//flag为volatile变量

x = 2;        //语句1
y = 0;        //语句2
flag = true;  //语句3
x = 4;        //语句4
y = -1;       //语句5

Comme la variable indicateur est une variable volatile, dans le processus de réorganisation des instructions, l'instruction 3 ne sera pas placée avant l'instruction 1 et l'instruction 2, et l'instruction 3 ne sera pas placée après l'instruction 4 et l'instruction 5.

Mais notez que l'ordre des instructions 1 et 2, ainsi que l'ordre des instructions 4 et 5 ne sont pas garantis.

Et le mot clé volatile peut garantir que lorsque l'instruction 3 est exécutée, l'instruction 1 et l'instruction 2 doivent être exécutées, et les résultats d'exécution de l'instruction 1 et de l'instruction 2 sont visibles pour l'instruction 3, l'instruction 4 et l'instruction 5.

  • Exemple deux
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

Dans l'exemple précédent, il a été mentionné que l'instruction 2 sera exécutée avant l'instruction 1, de sorte que le contexte n'a pas été initialisé depuis longtemps et que le contexte non initialisé est utilisé dans le thread 2 pour fonctionner, provoquant des erreurs de programme.

Si la variable inited est modifiée avec le mot-clé volatile, ce type de problème ne se produira pas, car lorsque l'instruction 2 est exécutée, il faut garantir que le contexte a été initialisé.

Scénarios d'utilisation courants

Le mot-clé volatile a de meilleures performances que synchronisé dans certains cas,

Mais notez que le mot clé volatile ne peut pas remplacer le mot clé synchronized, car le mot clé volatile ne peut pas garantir l'atomicité des opérations.

De manière générale, l'utilisation de volatile doit répondre aux deux conditions suivantes:

  1. Les opérations d'écriture sur des variables ne dépendent pas de la valeur actuelle

  2. La variable n'est pas incluse dans un invariant avec d'autres variables

En fait, ces conditions indiquent que les valeurs effectives qui peuvent être écrites dans des variables volatiles sont indépendantes de l'état de tout programme, y compris l'état actuel de la variable.

En fait, je crois comprendre que les deux conditions ci-dessus doivent garantir que l'opération est atomique afin de garantir que le programme utilisant le mot-clé volatile peut être exécuté correctement en même temps.

Scénarios courants

  • Indicateur d'état
volatile boolean flag = false;

while(!flag){
    doSomething();
}

public void setFlag() {
    flag = true;
}
  • Double contrôle Singleton
public class Singleton{
    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {
        if(instance==null) {
            synchronized (Singleton.class) {
                if(instance==null)
                    instance = new Singleton();
            }
        }
        return instance;
    }
}

Améliorations du JSR-133

Dans l'ancien modèle de mémoire Java avant JSR-133, bien que la réorganisation entre les variables volatiles n'était pas autorisée, l'ancien modèle de mémoire Java permettait la réorganisation entre les variables volatiles et les variables ordinaires.

Dans l'ancien modèle de mémoire, l'exemple de programme VolatileExample peut être réorganisé pour s'exécuter dans l'ordre suivant:

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;                      //1
        flag = true;                //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a;            //4
        }
    }
}
  • chronologie
时间线:----------------------------------------------------------------->
线程 A:(2)写 volatile 变量;                                  (1)修改共享变量 
线程 B:                    (3)读取 volatile 变量; (4)读共享变量

Dans l'ancien modèle de mémoire, lorsqu'il n'y a pas de dépendance de données entre 1 et 2, il est possible de réorganiser entre 1 et 2 (3 et 4 sont similaires).

Le résultat est: lorsque le thread de lecture B exécute 4, il ne voit pas nécessairement la modification de la variable partagée par le thread d'écriture A lorsqu'il exécute 1.

Par conséquent, dans l'ancien modèle de mémoire, l'écriture-lecture volatile n'a pas la sémantique de la mémoire d'acquisition de libération de moniteur.

Afin de fournir un mécanisme plus léger pour la communication entre les threads que les verrous de moniteur,

Le groupe d'experts JSR-133 a décidé d'améliorer la sémantique mémoire de volatile:

Limitez strictement la réorganisation des variables volatiles et des variables ordinaires par le compilateur et le processeur, et assurez-vous que l'écriture-lecture volatile et l'acquisition de libération de surveillance ont la même sémantique de mémoire.

Du point de vue des règles de réorganisation du compilateur et de la stratégie d'insertion de la barrière mémoire du processeur, tant que la réorganisation entre les variables volatiles et les variables ordinaires peut détruire la sémantique mémoire de volatile,
cette réorganisation sera réorganisée par les barrières mémoire du compilateur et du processeur. La politique d'insertion est interdite.

principe de mise en œuvre volatile

Définition des termes

le terme vocabulaire anglais la description
Variable partagée Variables partagées Les variables qui peuvent être partagées entre plusieurs threads sont appelées variables partagées. Les variables partagées incluent toutes les variables d'instance, les variables statiques et les éléments de tableau. Ils sont tous stockés dans la mémoire du tas, volatile n'agit que sur les variables partagées
Barrière de mémoire Barrières de mémoire Est un ensemble d'instructions du processeur utilisé pour limiter l'ordre des opérations de mémoire
Ligne tampon Cache line La plus petite unité de stockage pouvant être allouée dans le cache. Lorsque le processeur remplit la ligne de cache, il charge toute la ligne de cache, ce qui nécessite plusieurs cycles de lecture de la mémoire principale
Manipulation atomique Opérations atomiques Une opération ou une série d'opérations sans interruption
Remplissage de la ligne de cache remplissage de ligne de cache Lorsque le processeur reconnaît que l'opérande lu à partir de la mémoire peut être mis en cache, le processeur lit toute la ligne de cache dans le cache approprié (L1, L2, L3 ou tout)
Cache hit cache hit Si l'emplacement de la mémoire pour l'opération de remplissage de la ligne de cache est toujours l'adresse accédée par le processeur la prochaine fois, le processeur lit l'opérande à partir du cache au lieu de la mémoire
Écriture hit écrire hit Lorsque le processeur réécrit l'opérande dans une zone de cache mémoire, il vérifie d'abord si l'adresse mémoire du cache se trouve dans la ligne de cache. S'il existe une ligne de cache valide, le processeur réécrit l'opérande dans le cache. Au lieu de réécrire en mémoire, cette opération est appelée un succès d'écriture
Disparu l'écriture manque le cache Une ligne de cache valide est écrite dans une zone mémoire qui n'existe pas

principe

Alors, comment la volatilité garantit-elle la visibilité?

Sous le processeur x86, utilisez l'outil pour obtenir les instructions d'assemblage générées par le compilateur JIT pour voir ce que fera le CPU lors de l'écriture de volatile.

  • Java
instance = new Singleton();//instance是volatile变量

Assemblage correspondant

0x01a3de1d: movb $0x0,0x1104800(%esi);
0x01a3de24: lock addl $0x0,(%esp);

Lors de l'écriture d'une variable partagée modifiée avec une variable volatile, il y aura une deuxième ligne de code d'assemblage.
En vérifiant le manuel du développeur du logiciel d'architecture IA-32, on peut savoir que les lockinstructions préfixées causeront deux choses sous les processeurs multicœurs.

  • Les données de la ligne de cache actuelle du processeur seront réécrites dans la mémoire système.

  • Cette opération de réécriture dans la mémoire invalidera les données mises en cache à cette adresse mémoire dans d'autres CPU.

Afin d'améliorer la vitesse de traitement, le processeur ne communique pas directement avec la mémoire, mais lit d'abord les données de la mémoire système dans le cache interne (L1, L2 ou autre) avant d'effectuer l'opération, mais après l'opération, on ne sait pas quand elles seront écrites dans la mémoire ,

Si une variable volatile est déclarée pour une opération d'écriture, la JVM enverra une instruction de préfixe de verrouillage au processeur pour écrire les données dans la ligne de cache où la variable se trouve dans la mémoire système.

Mais même s'il est réécrit dans la mémoire, si les valeurs mises en cache des autres processeurs sont encore anciennes, il y aura des problèmes lors des opérations de calcul.

Ainsi, sous multiprocesseurs, afin de s'assurer que les caches de chaque processeur sont cohérents, un protocole de cohérence du cache sera implémenté.Chaque processeur vérifie si la valeur de son propre cache a expiré en reniflant les données étalées sur le bus.
Lorsque le processeur constate que l’adresse mémoire correspondant à sa ligne de cache a été modifiée, il définit la ligne de cache du processeur actuel sur un état non valide. Lorsque le processeur souhaite modifier les données, il force le rechargement des données à partir de la mémoire système. Lisez dans le cache du processeur.

Visibilité

Ces deux choses sont expliquées en détail dans le chapitre sur la gestion des multiprocesseurs (chapitre 8) du troisième volume du Manuel d'architecture du développeur logiciel IA-32

L'instruction de préfixe de verrouillage entraîne la réécriture du cache du processeur dans la mémoire

L'instruction de préfixe Lock provoque le signal LOCK # du processeur vocal pendant l'exécution de l'instruction.

Dans un environnement multiprocesseur, le signal LOCK # garantit que le processeur peut utiliser exclusivement toute mémoire partagée pendant l'affirmation du signal. (Parce qu'il verrouille le bus, les autres processeurs ne peuvent pas accéder au bus. Le fait de ne pas accéder au bus signifie que la mémoire système n'est pas accessible.) Cependant, dans les processeurs récents, le signal LOCK # ne verrouille généralement pas le bus, mais verrouille le cache. Les frais généraux du bus sont relativement importants.

Il y a une description détaillée de l'impact de l'opération de verrouillage sur le cache du processeur au chapitre 8.1.4. Pour les processeurs Intel486 et Pentium, le signal LOCK # est toujours déclaré sur le bus pendant l'opération de verrouillage.

Mais dans les processeurs P6 et récents, si la zone mémoire accédée est mise en cache à l'intérieur du processeur, le signal LOCK # ne sera pas affirmé.

Au contraire, il verrouille le cache de cette zone mémoire et le réécrit en mémoire, et utilise un mécanisme de cohérence du cache pour assurer l'atomicité de la modification. Cette opération est appelée "verrouillage du cache". Le
mécanisme de cohérence du cache empêche les modifications simultanées d'être modifiées. Données de la zone de mémoire mises en cache par deux processeurs ou plus.

La réécriture du cache d'un processeur dans la mémoire invalidera le cache des autres processeurs

Les processeurs IA-32 et Intel 64 utilisent le protocole de contrôle MESI (modifier, exclure, partager, invalider) pour maintenir la cohérence du cache interne et des autres caches de processeur.

Lorsqu'ils fonctionnent dans un système de processeur multicœur, les processeurs IA-32 et Intel 64 peuvent renifler d'autres processeurs pour accéder à la mémoire système et à leurs caches internes.

Ils utilisent une technologie de reniflage pour garantir que les données de leur cache interne, de la mémoire système et des autres caches de processeur restent cohérentes sur le bus.

Par exemple, dans les processeurs de la famille Pentium et P6, si un processeur est reniflé pour détecter qu'un autre processeur a l'intention d'écrire une adresse mémoire, et que cette adresse traite actuellement l'état partagé,
le processeur qui renifle invalidera sa ligne de cache. Lors de l'accès à la même adresse mémoire en même temps, le remplissage de la ligne de cache est forcé.

Optimiser l'utilisation des volatiles

Doug Lea, le célèbre maître de programmation simultanée Java, a ajouté une classe de collecte de files d'attente au package simultané de JDK7. Lors de l'utilisation de variables volatiles LinkedTransferQueue,
il a utilisé un moyen d'ajouter des octets pour optimiser les performances de la file d'attente et de la mise en file d'attente.

Des octets supplémentaires peuvent-ils optimiser les performances? Cette méthode a l'air incroyable, mais si vous comprenez l'architecture du processeur en profondeur, vous pouvez comprendre le mystère.

Jetons un coup d' oeil à LinkedTransferQueuecette classe,
il utilise un type de classe interne pour définir la tête de la file d' attente file d' attente (tête) et le noeud de queue (queue),
et cette classe intérieure PaddedAtomicReference par rapport à la AtomicReference de classe parente seulement faire une chose, il sera La variable partagée est ajoutée à 64 octets .

Nous pouvons calculer que la référence à un objet occupe 4 octets, et elle ajoute 15 variables pour occuper un total de 60 octets, plus la variable Value de la classe parente, ce qui fait un total de 64 octets.

  • LinkedTransferQueue.java
/** head of the queue */
private transient final PaddedAtomicReference < QNode > head;

/** tail of the queue */

private transient final PaddedAtomicReference < QNode > tail;

static final class PaddedAtomicReference < T > extends AtomicReference < T > {

    // enough padding for 64bytes with 4byte refs 
    Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;

    PaddedAtomicReference(T r) {

        super(r);

    }

}

public class AtomicReference < V > implements java.io.Serializable {

    private volatile V value;

    //省略其他代码 
}

Pourquoi l'ajout de 64 octets peut améliorer l'efficacité de la programmation simultanée?

Parce que pour les processeurs Intel Core i7, Core, Atom et NetBurst, Core Solo et Pentium M, la ligne de cache du cache L1, L2 ou L3 a une largeur de 64 octets et ne prend pas en charge les lignes de cache partiellement remplies, ce qui signifie que si la file d'attente est Si le nœud de tête et le nœud de queue ont moins de 64 octets, le processeur les lira tous dans la même ligne de cache. Sous multiprocesseurs, chaque processeur mettra en cache les mêmes nœuds de tête et de queue. Lorsqu'un processeur tente de modifier Le contact de tête verrouille toute la ligne de cache, donc sous l'effet du mécanisme de cohérence du cache, les autres processeurs ne pourront pas accéder au nœud de queue dans leur propre cache, et les opérations d'entrée de file d'attente et de retrait de la file d'attente doivent constamment modifier la tête. Joints et nœuds de queue, donc dans le cas des multi-processeurs, cela affectera sérieusement l'efficacité de l'entrée et de la sortie de la file d'attente.

Doug lea remplit la ligne de cache du tampon haute vitesse en ajoutant à 64 octets, évitant que le nœud principal et le nœud final ne soient chargés dans la même ligne de cache, de sorte que les nœuds tête et queue ne se verrouillent pas mutuellement lorsqu'ils sont modifiés .

  • Alors devrait-il être ajouté à 64 octets lors de l'utilisation de variables volatiles?

non.

Cette méthode ne doit pas être utilisée dans les deux scénarios.

Premièrement: pour les processeurs avec des lignes de cache d'une largeur inférieure à 64 octets, tels que les processeurs de la série P6 et Pentium, leurs lignes de cache L1 et L2 ont une largeur de 32 octets.

Deuxièmement: les variables partagées ne seront pas écrites fréquemment.

Étant donné que la méthode d'utilisation d'octets supplémentaires nécessite que le processeur lise davantage d'octets dans la mémoire tampon haute vitesse, ce qui lui-même entraînera une certaine consommation de performances. Si la variable partagée n'est pas fréquemment écrite, les chances de verrouillage sont également très faibles. Il n'est pas nécessaire d'ajouter des octets pour éviter le verrouillage mutuel.

ps: Du coup je sens que je veux me spécialiser dans le domaine de l'art, la connaissance et la sagesse sont indispensables.

le fil double / long n'est pas sûr

L'une des nombreuses règles définies par la spécification de la machine virtuelle Java: toutes les opérations sur les types de base, à l'exception de certaines opérations sur les types longs et doubles, sont atomiques.

La JVM actuelle (machine virtuelle java) utilise 32 bits comme opérations atomiques, et non 64 bits.

Lorsque le thread lit la valeur de type long / double dans la mémoire principale dans la mémoire de thread, il peut s'agir de deux opérations d'écriture de la valeur 32 bits. Évidemment, si plusieurs threads fonctionnent en même temps, il peut y avoir deux 32 bits haut et bas. Une erreur de valeur se produit.

Pour partager des champs longs et doubles entre les threads, ils doivent être exploités de manière synchronisée ou déclarés comme volatils.

résumé

Volatile, en tant que mot clé très important dans JMM, est essentiellement un point de connaissance qui doit être demandé pour les entretiens à haute concurrence.

J'espère que cet article vous sera utile pour votre entretien travail-étude. Si vous avez d'autres idées, vous pouvez également les partager avec vous dans la section commentaires.

Les goûts, les favoris et les attaquants des geeks sont la plus grande motivation pour l’écriture de Ma!

Pour un contenu plus passionnant, vous pouvez suivre [Lao Ma Xiaoxifeng]

Après avoir travaillé pendant 5 ans, je ne connais pas le mot clé volatile?

Je suppose que tu aimes

Origine blog.51cto.com/9250070/2542681
conseillé
Classement