5 cas et organigrammes vous aident à comprendre le mot clé volatile de 0 à 1

volatil

Avec l'amélioration du matériel, le nombre de cœurs de la machine est passé de monocœur à multicœur. Afin d'améliorer l'utilisation de la machine, la programmation simultanée est devenue de plus en plus importante et est devenue une priorité absolue au travail et lors des entretiens.Afin de mieux comprendre et utiliser la programmation simultanée, vous devez créer votre propre système de connaissances en programmation simultanée Java.

Cet article se concentrera sur le mot-clé volatile en Java, décrivant l'atomicité, la visibilité, l'ordre, le rôle de volatile, les principes de mise en œuvre, les scénarios d'utilisation et les problèmes associés tels que JMM et le pseudo-partage d'une manière simple et facile à comprendre.

Afin de mieux décrire le volatile, parlons d’abord de ses connaissances préalables : l’ordre, la visibilité et l’atomicité.

Ordre

Qu’est-ce que l’ordre ?

Lorsque nous utilisons des langages de haut niveau et une syntaxe simple pour la programmation, nous devons finalement traduire le langage en instructions que le CPU comprend.

Puisque c'est le CPU qui fait le travail, afin d'accélérer l'utilisation du CPU, les instructions pour notre contrôle de processus seront réorganisées.

Dans le modèle de mémoire Java, les règles de réorganisation des instructions doivent satisfaire à la règle de l'occurrence avant. Par exemple, le démarrage d'un thread doit avoir lieu avant les autres opérations du thread. Les instructions qui démarrent le thread ne peuvent pas être réorganisées vers le thread. les tâches effectuées

C'est-à-dire que dans le modèle de mémoire Java, la réorganisation des instructions n'affectera pas le processus d'exécution monothread que nous avons spécifié , mais dans le cas du multithreading, le processus d'exécution de chaque thread ne peut pas être estimé.

Pour une description plus appropriée, regardez le morceau de code suivant

    static int a, b, x, y;

    public static void main(String[] args){
        long count = 0;
        while (true) {
            count++;
            a = 0;b = 0;x = 0;y = 0;
            Thread thread1 = new Thread(() -> {
                a = 1;
                x = b;
            });
            Thread thread2 = new Thread(() -> {
                b = 1;
                y = a;
            });
            thread1.start();
            thread2.start();

            try {
                thread1.join();
                thread2.join();
            } catch (Exception e) {}

            if (x == 0 && y == 0) {
                break;
            }
        }
        //count=118960,x=0,y=0
        System.out.println("count=" + count + ",x=" + x + ",y=" + y);
    }

Initialisez les quatre variables a, b, x et y à 0

Selon notre réflexion, l'ordre d'exécution est

//线程1
a = 1;
x = b;

//线程2
b = 1;
y = a;

Cependant, une fois les instructions réorganisées, quatre situations peuvent se produire :

//线程1
//1           2           3           4     
a = 1;      a = 1;      x = b;      x = b;        
x = b;      x = b;      a = 1;      a = 1;  

//线程2
//1           2           3           4 
b = 1;      y = a;      b = 1;      y = a;
y = a;      b = 1;      y = a;      b = 1;

Lorsque la quatrième situation se produit, x et y peuvent tous deux être 0

Alors, comment pouvons-nous garantir l’ordre ?

Utilisez volatile pour modifier les variables afin d'assurer l'ordre

Afin d'améliorer l'utilisation du processeur, les instructions seront réorganisées. La réorganisation ne peut garantir que la logique d'exécution du processus sous un seul thread.

L'ordre d'exécution ne peut pas être prédit sous plusieurs threads, et l'ordre ne peut pas être garanti. Si vous souhaitez garantir l'ordre sous multi-threads, vous pouvez utiliser volatile. Volatile utilisera des barrières de mémoire pour interdire la réorganisation des instructions afin d'atteindre l'ordre.

Vous pouvez également verrouiller directement pour garantir une exécution synchrone.

Dans le même temps, vous pouvez utiliser Unsafela barrière mémoire de la classe dans le package concurrent pour interdire la réorganisation.

//线程1
a = 1;
unsafe.fullFence();
x = b;

//线程2
b = 1;   
unsafe.fullFence();
y = a;

visibilité

Qu'est-ce que la visibilité ?

Dans le modèle de mémoire Java, chaque thread possède sa propre mémoire de travail et sa propre mémoire principale. Lors de la lecture des données, elles doivent être copiées de la mémoire principale vers la mémoire de travail. Lors de la modification des données, elles ne sont modifiées que dans leur propre mémoire de travail. Si plusieurs threads Si certaines données sont manipulées en même temps et que la modification n'est pas réécrite dans la mémoire principale, les autres threads ne pourront pas détecter les modifications des données.

image-20230823223141451.png

Par exemple, dans le morceau de code suivant, le thread créé continuera à boucler car il ne peut pas détecter que d'autres threads modifient les variables.

    //nonVolatileNumber 是未被volatile修饰的
    new Thread(() -> {
        while (nonVolatileNumber == 0) {
    
        }
    }).start();
    
    TimeUnit.SECONDS.sleep(1);
    nonVolatileNumber = 100;

Alors comment rendre cette variable visible ?

Vous pouvez utiliser une modification volatile sur cette variable pour assurer la visibilité, ou vous pouvez la verrouiller de manière synchronisée. Après le verrouillage, cela équivaut à relire les données dans la mémoire principale.

atomicité

Qu’est-ce que l’atomicité ?

En fait, il s'agit de savoir si une ou plusieurs opérations peuvent être réalisées en même temps, sinon elles échoueront toutes, plutôt que certaines d'entre elles réussissent et d'autres échouent.

L'atomicité des instructions de lecture et de chargement (ci-dessus) dans le modèle de mémoire Java est implémentée par la machine virtuelle

Grâce à l'auto-incrémentation d'une variable, celle-ci doit d'abord être lue dans la mémoire principale, puis modifiée et enfin réécrite dans la mémoire principale.

Alors, la volatilité peut-elle garantir l’atomicité ?

Nous utilisons deux threads pour incrémenter la même variable modifiée avec volatile dix mille fois.

        private volatile int num = 0;
    
        public static void main(String[] args) throws InterruptedException {
            C_VolatileAndAtomic test = new C_VolatileAndAtomic();
            Thread t1 = new Thread(() -> {
                forAdd(test);
            });
    
            Thread t2 = new Thread(() -> {
                forAdd(test);
            });
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            //13710
            System.out.println(test.num);
        }
    
        /**
         * 循环自增一万次
         *
         * @param test
         */
        private static void forAdd(C_VolatileAndAtomic test) {
            for (int i = 0; i < 10000; i++) {
                test.num++;
            }
        }

Malheureusement, le résultat n’est pas 20 000, ce qui indique que les variables volatiles modifiées ne peuvent pas garantir leur atomicité.

Alors, quelle méthode peut garantir l’atomicité ?

La méthode de verrouillage synchronisé peut garantir l'atomicité, car elle seule peut accéder au verrou en même temps.

En utilisant des classes atomiques, la méthode sous-jacente d'utilisation de CAS peut également garantir l'atomicité. Nous en reparlerons dans les prochains articles

principe volatil

Après avoir décrit et testé l'ordre, la visibilité et l'atomicité, nous pouvons savoir que volatile peut garantir l'ordre et la visibilité, mais il ne peut pas garantir l'atomicité .

Alors, comment la couche inférieure volatile parvient-elle à assurer l’ordre et la visibilité ?

La JVM ajoutera des indicateurs d'accès volatiles aux variables modifiées avec volatile et utilisera la barrière de mémoire du système d'exploitation pour interdire la réorganisation des instructions lorsque des instructions de bytecode sont exécutées.

La barrière de mémoire universelle couramment utilisée est storeload, store1 storeload load2, qui interdit la réorganisation des instructions d'écriture en dessous de la barrière et interdit la réorganisation des instructions de lecture au-dessus de la barrière. C'est-à-dire que la mémoire réécrite par le magasin est visible (perceptible) vers d'autres processeurs. Les lectures de chargement ultérieures seront lues à partir de la mémoire.

L'implémentation de l'instruction d'assemblage volatile est en fait une instruction de préfixe de verrouillage

L'instruction de préfixe de verrouillage n'a aucun impact sur un seul cœur, car un seul cœur peut garantir l'ordre, la visibilité et l'atomicité.

L'instruction de préfixe de verrouillage réécrira les données dans la mémoire lors de leur modification en mode multicœur. La réécriture dans la mémoire doit garantir qu'un seul processeur fonctionne en même temps. Cela peut être fait en verrouillant le bus, mais d'autres processeurs ne peut pas y accéder.

Afin d'améliorer la granularité de la concurrence, le processeur prend en charge le verrouillage du cache (uniquement le verrouillage des lignes de cache) et utilise le protocole de cohérence du cache pour garantir que les mêmes données de ligne de cache ne peuvent pas être modifiées en même temps.

Après la réécriture dans la mémoire, la technologie de reniflage est utilisée pour permettre à d'autres processeurs de détecter les modifications des données et de relire la mémoire avant une utilisation ultérieure.

Problème de faux partage

Puisque chaque lecture est une opération sur une ligne de cache, si plusieurs threads modifient fréquemment deux variables dans la même ligne de cache, cela obligera-t-il les autres processeurs qui utilisent la ligne de cache à toujours avoir besoin de relire les données ?

C’est en fait ce qu’on appelle le problème du pseudo-partage

Par exemple, deux variables i1 et i2 se trouvent dans la même ligne de cache. Le processeur 1 écrit fréquemment dans i1 et le processeur 2 écrit fréquemment dans i2. i1 et i2 sont tous deux modifiés par volatile, ce qui entraîne également la modification de i1. Lorsque, le processeur 2 détecte que la ligne de cache est sale, il doit donc relire la mémoire pour obtenir la dernière ligne de cache, mais une telle surcharge de performances n'a aucun sens pour que le processeur 2 écrive sur i2.

image-20230824210222603.png

Une manière courante de résoudre le problème du faux partage consiste à ajouter suffisamment de champs entre ces deux champs afin qu'ils ne se trouvent pas sur la même ligne de cache, ce qui entraînera également une perte d'espace.

Afin de résoudre le problème du faux partage, JDK fournit également @sun.misc.Contendeddes annotations pour nous aider à remplir les champs.

Le code suivant fait boucler deux threads 1 milliard de fois pour effectuer une auto-incrémentation. Lorsqu'un faux problème de partage se produit, cela prend plus de 30 secondes. Lorsqu'un faux problème de partage ne se produit pas, cela ne prend que quelques secondes.

Il convient de noter que @sun.misc.Contendedlors de l'utilisation d'annotations, vous devez transporter les paramètres JVM-XX:-RestrictContended

        @sun.misc.Contended
        private volatile int i1 = 0;
        @sun.misc.Contended
        private volatile int i2 = 0;
        
        public static void main(String[] args) throws InterruptedException {
            D_VolatileAndFalseSharding test = new D_VolatileAndFalseSharding();
            int count = 1_000_000_000;
            Thread t1 = new Thread(() -> {
                for (int i = 0; i < count; i++) {
                    test.i1++;
                }
            });
    
            Thread t2 = new Thread(() -> {
                for (int i = 0; i < count; i++) {
                    test.i2++;
                }
            });
    
            long start = System.currentTimeMillis();
    
            t1.start();
            t2.start();
    
            t1.join();
            t2.join();
    
            //31910 i1:1000000000 i2:1000000000
    
            //使用@sun.misc.Contended解决伪共享问题  需要携带JVM参数 -XX:-RestrictContended
            //5961 i1:1000000000 i2:1000000000
            System.out.println((System.currentTimeMillis() - start) + " i1:"+ test.i1 + " i2:"+ test.i2);
        }

scénarios d'utilisation volatiles

Volatile interdit la réorganisation des instructions à travers les barrières de mémoire pour garantir la visibilité et l'ordre.

Sur la base des caractéristiques de visibilité, volatile est très approprié pour une utilisation dans des scénarios de lecture en programmation simultanée, car volatile garantit la visibilité, et avec les opérations de lecture sans verrouillage , la surcharge est très faible.

Par exemple : l'état de synchronisation de la file d'attente AQS dans le package simultané sera modifié avec volatile

Les opérations d'écriture nécessitent souvent des garanties de synchronisation de verrouillage

Sur la base des caractéristiques d'ordre, volatile peut interdire la réorganisation des instructions de création d'objets lors de la double vérification des verrous, empêchant ainsi d'autres threads d'acquérir des objets qui n'ont pas encore été initialisés.

La création d'un objet peut être divisée en trois étapes :

//1.分配内存
//2.初始化对象
//3.将对象指向分配的空间

Étant donné que les étapes 2 et 3 dépendent toutes deux de l'étape 1, l'étape 1 ne peut pas être réorganisée et les étapes 2 et 3 n'ont aucune dépendance. La réorganisation peut donc entraîner le pointage de l'objet vers l'espace alloué d'abord, puis son initialisation.

Si, à ce moment-là, dans le verrou de double détection, un thread juge qu'il n'est pas vide et utilise cet objet, mais qu'il n'a pas été initialisé à ce moment-là, il peut y avoir un problème car l'objet obtenu n'a pas encore été initialisé.

Par conséquent, le verrou de double vérification correct doit ajouter du volatile pour interdire la réorganisation.

        private static volatile Singleton singleton;
    
        public static Singleton getSingleton(){
            if (Objects.isNull(singleton)){
                //有可能很多线程阻塞到拿锁,拿完锁再判断一次
                synchronized (Singleton.class){
                    if (Objects.isNull(singleton)){
                        singleton = new Singleton();
                    }
                }
            }
    
            return singleton;
        }

Résumer

Cet article se concentre sur le mot-clé volatile pour décrire l'ordre, la visibilité, l'atomicité, JMM, les principes volatiles, les scénarios d'utilisation, les problèmes de pseudo-partage, etc.

Afin d'améliorer l'utilisation du processeur, les instructions seront réorganisées. La réorganisation n'affectera pas le processus de pointage sous un seul thread, mais le processus d'exécution sous multithread ne peut pas être prédit.

Dans le modèle de mémoire Java, chaque thread possède sa propre mémoire de travail. La lecture des données doit être lue dans la mémoire principale et la modification des données doit être réécrite dans la mémoire principale ; en programmation simultanée, lorsque les autres threads ne peuvent pas sentir que le la variable a été modifiée, si vous continuez à l'utiliser, vous risquez de faire une erreur

Volatile interdit la réorganisation des instructions à travers les barrières de mémoire pour assurer l'ordre et la visibilité, mais ne peut pas satisfaire l'atomicité.

L'assemblage sous-jacent de volatile est implémenté à l'aide de l'instruction de préfixe de verrouillage. Lors de la modification des données sous multicœur, le bus sera verrouillé pour réécrire les données dans la mémoire. En raison du coût élevé du verrouillage du bus, la ligne de cache sera verrouillé plus tard, et le protocole de cohérence du cache sera utilisé pour garantir qu'un seul processus peut être traité en même temps. Le processeur modifie la même ligne de cache et utilise la technologie de reniflage pour permettre aux autres processeurs propriétaires de la ligne de cache de percevoir que le la ligne de cache est sale et relisez-la ensuite.

Si les variables fréquemment écrites par plusieurs threads sont dans la même ligne de cache, des problèmes de faux partage se produiront. A ce moment, vous devez remplir les champs pour qu'ils ne soient pas dans la même ligne de cache.

Sur la base des caractéristiques de visibilité, volatile est souvent utilisé pour implémenter des opérations de lecture sans verrouillage dans la programmation simultanée ; sur la base des caractéristiques d'ordre, il peut garantir que l'instance obtenue n'est pas non initialisée dans les verrous à double détection.

Enfin (ne le faites pas gratuitement, appuyez simplement trois fois de suite pour demander de l'aide~)

Cet article est inclus dans la colonne " Du point à la ligne et de la ligne à la surface " pour construire un système de connaissances en programmation simultanée Java en termes simples . Les étudiants intéressés peuvent continuer à y prêter attention.

Les notes et les cas de cet article ont été inclus dans gitee-StudyJava et github-StudyJava . Les étudiants intéressés peuvent continuer à y prêter attention sous stat~

Adresse du dossier :

Gitee-JavaConcurrentProgramming/src/main/java/A_volatile

Github-JavaConcurrentProgramming/src/main/java/A_volatile

Si vous avez des questions, vous pouvez en discuter dans la zone de commentaires. Si vous pensez que l'écriture de Cai Cai est bonne, vous pouvez l'aimer, la suivre et la collecter pour la soutenir ~

Lei Jun : La version officielle du nouveau système d'exploitation de Xiaomi, ThePaper OS, a été emballée. Une fenêtre contextuelle sur la page de loterie Gome App insulte son fondateur. Le gouvernement américain restreint l'exportation du GPU NVIDIA H800 vers la Chine. L'interface Xiaomi ThePaper OS est exposé. Un maître a utilisé Scratch pour frotter le simulateur RISC-V et il a fonctionné avec succès. Noyau Linux RustDesk Remote Desktop 1.2.3 publié, prise en charge améliorée de Wayland Après avoir débranché le récepteur USB Logitech, le noyau Linux a planté. DHH examen approfondi des "outils d'emballage" " : le front-end n'a pas du tout besoin d'être construit (No Build) JetBrains lance Writerside pour créer de la documentation technique Outils pour Node.js 21 officiellement publié
{{o.name}}
{{m.nom}}

Je suppose que tu aimes

Origine my.oschina.net/u/6903207/blog/10112325
conseillé
Classement