Pourquoi les verrous de double vérification doivent-ils utiliser le mot clé volatile?

Description avant

C'est ma propre compréhension des problèmes liés à l'utilisation de verrous vérifiés deux fois et du rôle de la sémantique volatile dans ceux-ci.
Il s'agit d'un article documentaire personnel, qui peut être un peu simple dans la description de la langue, et ne citera pas trop d'explications ou d'explications de terminologie technique officielle.
S'il y a quelque chose qui ne va pas, veuillez laisser un message pour correction.
Commencez le texte ci-dessous:

Une implémentation singleton

S'il existe une classe Earth (Earth), nous devons en fournir une implémentation singleton paresseuse. Une manière conventionnelle d'écrire est la suivante:

public class Earth {
    
    
    private static Earth earth = null;

    public static Earth getInstance() {
    
    
        if (earth == null) {
    
    
            earth = new Earth();
        }
        return earth;
    }
}

Dans un environnement à un seul thread, ce type d'écriture est totalement exempt de problèmes. Il n'est pas nécessaire de prendre en compte les problèmes de concurrence et il n'y aura pas de sections critiques.
Malheureusement, en réalité, les processeurs de nos ordinateurs sont généralement multicœurs et les programmes doivent souvent s'exécuter simultanément avec plusieurs threads. Dans un tel environnement, si plusieurs threads s'exécutent dans cette méthode en même temps, ce code peut créer plusieurs objets et allouer de la mémoire à plusieurs reprises. De même, le mode singleton peut être accompagné d'une initialisation de logique métier complexe dans des applications pratiques, et cette initialisation répétée ne sera pas autorisée.
Ensuite, la solution est de synchroniser les opérations. Par exemple: verrouiller cette méthode

    public static synchronized Earth getInstance() {
    
    
        if (earth == null) {
    
    
            earth = new Earth();
        }
        return earth;
    }

Bien sûr, sans tenir compte des performances, ce code doit garantir la sécurité des threads. Lorsque le thread qui obtient le verrou entre pour la première fois dans cette méthode de synchronisation, si l'objet Terre est vide, il construira un objet et l'attribuera, puis d'autres Après le thread entre, l'objet a été créé. Selon la sémantique de synchronized, il n'y aura aucun problème ici.
Cependant, dans le processus de développement proprement dit, on s'attend à ce que la granularité du verrou soit aussi petite que possible pour réduire le temps de blocage de l'attente du verrou. Par conséquent, l'opération synchronisée est placée dans le corps de la méthode pour synchroniser une partie du code. C'est ce que j'ai dit ensuite, vérifiez le verrouillage.

Verrouillage à double contrôle

Un exemple de code incorrect:

public class Earth {
    
    
    private static Earth earth = null;

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

Il s'agit d'un verrou à double vérification.Déterminez d'abord si la terre est vide, si elle est vide, entrez dans le bloc de synchronisation, puis déterminez si elle est vide et toujours vide, puis un nouvel objet lui est donné. De cette façon, lorsque d'autres threads entrent dans le corps de cette méthode, s'ils trouvent que l'objet a été créé et n'est pas vide, ils ne seront pas bloqués en attente dans le bloc de synchronisation, mais exécuteront directement le code suivant. Bien sûr, le code suivant dans l'exemple n'a pas de logique. Revenez simplement à l'instance de terre directement.
L'objet de verrouillage du bloc de synchronisation ps n'est pas recommandé d'utiliser Earth.class, c'est-à-dire que le verrou de l'objet de classe de la classe appartient, la plage est trop grande, en fait, le bloc de synchronisation de méthode statique peut également l'utiliser . Il est préférable de créer un nouvel objet en tant qu'objet de verrouillage.
Bien sûr, l'exemple de code ci-dessus est incorrect, car l'attribut earth déclaré n'utilise pas le mot-clé volatile. Par conséquent, il y a un problème avec ce verrouillage de double vérification. Pourquoi y a-t-il un problème? Ce qui suit explique.

Je l'ai tellement mal utilisé

Quand je viens de terminer mes études, je ne connaissais pas assez certaines des spécifications Java. Le verrou de double vérification a été écrit de la manière ci-dessus. Par exemple, lors de la déclaration de l'attribut earth, je ne savais pas comment utiliser volatile. Un jour, alors que j'écrivais du code, un senior m'a vu écrire des verrous de double vérification et m'a dit que le mot-clé volatile devait être ajouté pour déclarer les propriétés de l'objet singleton à créer. J'ai juste demandé pourquoi. Il m'a dit qu'il devrait éviter de «réorganiser» des termes techniques que je ne comprenais pas à l'époque. Je ne m'en dis pas plus, je dis simplement que j'ai vu le problème de réorganisation sur certains matériaux. Ce que j'ai dit était hésitant, et j'étais également déconcerté par ce que j'ai entendu!
Plus tard, j'ai aussi lu beaucoup de documents ou de livres liés à la JVM.Bien sûr, comme le temps a passé si longtemps, je ne me souviens plus du contenu spécifique des livres que j'ai lus pendant cette période. N'oubliez pas d'écrire un verrou de double vérification pour ajouter une instruction volatile.

Paradigme d'écriture correct

public class Earth {
    
    
    private static volatile Earth earth = null;

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

En ce qui concerne volatile, je me souviens toujours de deux termes qui y sont liés: visibilité et interdiction de réapprovisionnement.
Alors, quelle est exactement la visibilité et l'interdiction de réorganiser dans la sémantique volatile?

volatil

En ce qui concerne la «visibilité» et la «réorganisation», pour certains étudiants qui ne sont pas bien fondés, ils peuvent ne pas comprendre ce qu'ils veulent dire. Voici quelques explications simples, qui peuvent ne pas être professionnelles, mais essayez d'être aussi simple que possible et espérez être faciles à comprendre.
Trois problèmes courants dans la programmation simultanée: visibilité, atomicité et ordre.

  • Les
    termes atomicité doivent être entendus fréquemment. L'atomicité n'est pas le sujet de cet article, et elle n'est pas dans le cadre de la discussion. Si vous avez besoin de comprendre en détail, vous pouvez trouver des informations pertinentes. En termes simples: notre ligne de code dans java peut avoir besoin d'être expliqué. Pour que plusieurs instructions machine soient exécutées par le CPU (par exemple: num + = 1), ces instructions peuvent changer de thread pour exécuter d'autres instructions à tout moment pendant le processus d'exécution, et cette instruction n'a pas été exécutée pourtant, ce qui est un problème d'atomicité. A moins que ces instructions ne soient pas interrompues pendant l'exécution du CPU, c'est atomique.
  • Visibilité
    Il s'agit de la visibilité de la mémoire. Par exemple, il existe une variable:
int num = 0;

Il y a le thread A et le thread B, le thread A définit la variable num = 1, puis lorsque le thread B lit la valeur de num, la valeur lue est toujours 0. Après l'opération de lecture de B, l'opération d'écriture de A, mais B lit Ce n'est pas le 1 que vient d'écrire A. Ce problème pourrait-il exister? Oui, ça existe.
Il s'agit d'un problème de cohérence causé par la mise en cache matérielle. Il y a donc JMM (java memory model) pour résoudre ce problème de cohérence du cache.
Dans le JMM dont nous parlons souvent, un terme sera mentionné, appelé la mémoire locale (cache local) du thread java, dont dispose chaque thread. Il existe de nombreux matériaux, et on dit aussi que lors de l'exécution de code dans un thread, lorsque la valeur d'une variable est lue, elle sera d'abord chargée de la mémoire principale vers la mémoire locale. Une fois la valeur de la variable modifiée, la mémoire locale est modifiée, la valeur de sera alors réécrite dans la mémoire principale. Si avant de réécrire dans la mémoire principale, si un autre thread lit la valeur de cette variable, un autre thread ne pourra bien sûr pas obtenir la dernière valeur mise à jour dans la mémoire locale de ce thread, car peu importe si l'autre thread provient de sa propre La mémoire locale lue ou la mémoire principale n'est pas la dernière valeur, ce qui est le problème de visibilité de la mémoire mentionné ci-dessus.
Quand je suis entré en contact pour la première fois avec le concept de mémoire locale, je ne comprenais pas vraiment ce qu'est cette mémoire locale, et de nombreux étudiants novices devraient l'être aussi.
Cette mémoire principale est notre mémoire physique.
Cette mémoire locale est en fait un concept abstrait en Java, elle inclut le cache CPU, les registres ou autre matériel, l'optimisation de la compilation, etc. Ce n'est pas un espace mémoire ouvert par chaque thread séparément. Cependant, JMM discute en fait du concept abstrait de mémoire locale.
Par conséquent, chaque fois que java lit la valeur d'une variable, elle est d'abord lue à partir du cache local, puis chargée de la mémoire principale vers la mémoire locale si ce n'est pas le cas. L'écriture est également écrite ici en premier, puis synchronisée avec la mémoire principale. Mémoire.
La visibilité de volatile garantit que chaque lecture est obtenue à partir de la mémoire principale et est synchronisée avec la mémoire principale après l'écriture, afin que chaque thread puisse voir la dernière valeur mise à jour par d'autres threads.

  • Ordre Le
    problème d'ordre est vraiment contre-intuitif. Nous pensons que le code qui est exécuté séquentiellement doit être exécuté de haut en bas et d'avant en arrière, parfois le résultat n'est pas attendu.
    La raison est le problème de réorganisation causé par l'optimisation du compilateur, du runtime ou du CPU .
    Voici plusieurs réorganisations possibles:
  1. Réorganisation du compilateur ou de l'exécution, comme la suivante :, l' a = 1; b = 2;ordre de compilation peut être: b = 2; a = 1;(Voici une analogie, pour ne pas dire que la réorganisation de compilé en bytecode).
  2. Réorganisation matérielle, telle que la réorganisation lorsque le processeur exécute l'optimisation des instructions machine
  3. La réorganisation du système de stockage. Ceci est dû au fait que les données sont d'abord écrites dans le cache, pas immédiatement mises à jour dans la mémoire principale. Il existe des conditions et des conditions correspondantes, qui ne garantissent pas quel thread écrira la valeur de quelle variable en premier dans le cache., Il sera d'abord mis à jour dans la mémoire principale.
  4. Réorganisez dans d'autres cas.
    Ce problème de réorganisation des verrous à double vérification fait référence au deuxième type, un problème de réorganisation des instructions de la machine CPU.

Vérifier la réorganisation des instructions de verrouillage

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

Il s'agit du code permettant de créer l'objet avec le verrou de double vérification. Si deux threads A et B entrent dans cette méthode et trouvent que l'objet Terre est vide, ils commencent à se battre pour le verrou. Le thread A obtient le verrou et entre dans la synchronisation bloc, et le thread B attend le verrou. Une fois que A crée l'objet et libère le verrou, B obtient le verrou et constate que la terre n'est pas vide, il quitte donc le bloc de synchronisation et retourne à l'instance de la terre. Ce n'est pas un problème.
Cependant, earth = new Earth();cette ligne de code comporte plusieurs opérations:

  1. Allouer de l'espace mémoire
  2. Initialiser l'objet sur cette mémoire
  3. Attribuer l'adresse mémoire à la variable de terre

Après la réorganisation:

  1. Allouer de l'espace mémoire
  2. Attribuer l'adresse mémoire à la variable de terre
  3. Initialiser l'objet sur cette mémoire

Si le thread A effectue la deuxième action et que la terre n'est pas vide à ce moment, le thread A entre dans cette méthode pour déterminer si la terre est vide, et trouve qu'elle n'est pas vide, puis renvoie un objet qui n'a pas encore été initialisé, et ses propriétés peuvent être encore. Tous sont vides. Si vous accédez aux propriétés de membre de cette variable à ce stade, il peut y avoir des problèmes, tels qu'une exception de pointeur null.
L'image suivante est une capture d'écran d'un test similaire que j'ai trouvé pour illustrer ce problème. Les informations pertinentes sont liées à la fin:
Insérez la description de l'image ici

Comment volatile interdit la réorganisation

À l'heure actuelle, dans jdk1.5 à 1.8, seule la sémantique de volatile devrait inclure l'interdiction de réordonner les instructions. Synchronized n'est pas non plus pris en charge, il garantit simplement l'ordre et l'atomicité des blocs synchronisés. Son ordre est dû au fait qu'un seul thread exécute le code dans la section critique à un moment où le verrou est verrouillé, et cette atomicité signifie que le bloc de code verrouillé est atomique extérieurement, et peu importe si la réorganisation dans ce bloc de code est.
L'interdiction de la réorganisation volatile est l'utilisation de barrières de mémoire. La barrière de la mémoire consiste à ajouter des instructions associées avant et après la nécessité d'interdire la réorganisation. Je comprends cela de cette façon: le processeur réorganisera les instructions associées en raison de l'optimisation, ou s'il y a une exécution parallèle dans la méthode de pipeline, l'ajout d'une barrière de mémoire indique au CPU qu'aucun effort n'est nécessaire pour optimiser, étape par étape dans l'ordre. Par conséquent, la réorganisation est interdite, certaines optimisations ne seront pas utilisées et les performances doivent être dégradées.

Utilisez des scénarios de volatilité

Cet article décrit principalement l'utilisation des verrous à double contrôle. Il existe d'autres scénarios où la volatilité est requise. En raison de la garantie de visibilité de volatile, si l'opération d'écriture est trop fréquente, elle doit être utilisée avec précaution, ce qui entraînera une sérieuse dégradation des performances. Essayez d'utiliser le scénario avec plus de lectures et moins d'écritures.

Vérifiez le verrouillage avant JDK1.5

La sémantique de volatile pour interdire la réorganisation n'est disponible que dans les versions 1.5 (y compris 1.5) et ultérieures. Dans l'ancien JMM, volatile ne garantit que la visibilité. Par conséquent, à ce moment-là, il semble qu'il puisse y avoir des problèmes avec la façon d'écrire le verrou de double vérification, et ce n'est pas nécessairement sûr.J'ai même vu le schéma suivant, et il peut y avoir des problèmes dus à des raisons subtiles.

public class Earth {
    
    
    private static volatile Earth earth = null;

    public static Earth getInstance() {
    
    
        if (earth == null) {
    
    
            Earth tmp = null;
            synchronized (Earth.class) {
    
    
                tmp = earth;
                if (tmp == null) {
    
    
                    synchronized (Earth.class) {
    
    
                        tmp = new Earth();
                    }
                    earth = tmp;
                }
            }
        }
        return earth;
    }
}

Utilisez un singleton affamé

Bien sûr, dans un environnement multithread, il n'est pas nécessaire d'utiliser le verrou de double vérification pour créer un singleton. Il est recommandé d'utiliser le mode singleton statique de l'homme affamé pour créer une classe externe et utiliser l'objet singleton comme son statique champ.:

public class EarthHolder {
    
    

    private static final Earth EARTH = new Earth();

    public static Earth getInstance() {
    
    
        return EarthHolder.EARTH;
    }
}

Lecture recommandée

Vous pouvez lire les documents suivants. La capture d'écran de l'instruction de test ci-dessus se trouve ici:
http://gee.cs.oswego.edu/dl/cpj/jmm.html
http: //www.cs.umd. Edu / ~ pugh / java / memoryModel / DoubleCheckedLocking.html

Je suppose que tu aimes

Origine blog.csdn.net/x763795151/article/details/109015712
conseillé
Classement