Maison  >  Article  >  Java  >  Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

黄舟
黄舟original
2016-12-29 11:58:111354parcourir

Courses de données et garanties de cohérence séquentielle

Les courses de données existent lorsque les programmes ne sont pas synchronisés correctement. La spécification du modèle de mémoire Java définit la compétition de données comme suit :
écrire dans une variable dans un thread,
lire la même variable dans un autre thread,
et l'écriture et la lecture ne sont pas ordonnées par synchronisation.

Lorsque le code contient des courses de données, l'exécution du programme produit souvent des résultats contre-intuitifs (comme ce fut le cas dans l'exemple du chapitre précédent). Si un programme multithread est synchronisé correctement, le programme sera un programme sans course aux données.

JMM offre les garanties suivantes pour la cohérence de la mémoire des programmes multithread correctement synchronisés :
Si le programme est correctement synchronisé, l'exécution du programme sera séquentiellement cohérente – ​​c'est-à-dire que l'exécution du programme sera séquentiellement cohérent. Le résultat est le même que si le programme était exécuté dans un modèle de mémoire séquentiellement cohérent (comme nous le verrons bientôt, c'est une garantie extrêmement forte pour le programmeur). La synchronisation fait ici référence à la synchronisation au sens large, y compris l'utilisation correcte des primitives de synchronisation communes (lock, volatile et final).

Modèle de mémoire à cohérence séquentielle

Le modèle de mémoire à cohérence séquentielle est un modèle de référence théorique idéalisé par les informaticiens, qui offre aux programmeurs de fortes garanties de visibilité de la mémoire. Le modèle de mémoire à cohérence séquentielle présente deux caractéristiques majeures :
Toutes les opérations dans un thread doivent être exécutées dans l'ordre du programme.
(Que le programme soit synchronisé ou non) Tous les threads ne peuvent voir qu'un seul ordre d'exécution des opérations. Dans un modèle de mémoire séquentiellement cohérent, chaque opération doit être exécutée de manière atomique et immédiatement visible par tous les threads.

Le modèle de cohérence séquentielle fournit au programmeur la vue suivante :

Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

Conceptuellement, le modèle de cohérence séquentielle a une seule mémoire globale, cette mémoire peut être connecté à n’importe quel fil via un interrupteur qui oscille à gauche et à droite. Dans le même temps, chaque thread doit effectuer des opérations de lecture/écriture en mémoire dans l’ordre du programme. D'après la figure ci-dessus, nous pouvons voir qu'au plus un thread peut être connecté à la mémoire à tout moment. Lorsque plusieurs threads s'exécutent simultanément, le dispositif de commutation de la figure peut sérialiser toutes les opérations de lecture/écriture en mémoire de tous les threads.

Pour une meilleure compréhension, nous utilisons ci-dessous deux diagrammes schématiques pour expliquer plus en détail les caractéristiques du modèle de cohérence séquentielle.

Supposons que deux threads A et B s'exécutent simultanément. Le thread A comporte trois opérations, et leur ordre dans le programme est : A1->A2->A3. Le thread B comporte également trois opérations, et leur ordre dans le programme est : B1->B2->B3.

Supposons que les deux threads utilisent des moniteurs pour se synchroniser correctement : le thread A libère le moniteur après trois opérations, et le thread B acquiert ensuite le même moniteur. Ensuite, l'effet d'exécution du programme dans le modèle de cohérence séquentielle sera comme indiqué dans la figure ci-dessous :

Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

Supposons maintenant que les deux threads ne sont pas synchronisés. programme non synchronisé Schéma schématique d'exécution dans le modèle de cohérence séquentielle :

Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

Programme non synchronisé Dans le modèle de cohérence séquentielle, bien que l'ordre global d'exécution ne soit pas ordonné, tous les threads ne peuvent voir qu'à un séquence d’exécution globale cohérente. En prenant la figure ci-dessus comme exemple, l'ordre d'exécution vu par les threads A et B est : B1->A1->A2->B2->A3->B3. Cette garantie est obtenue car chaque opération dans un modèle de mémoire séquentiellement cohérent doit être immédiatement visible par n'importe quel thread.

Cependant, une telle garantie n’existe pas dans JMM. Non seulement l'ordre d'exécution global des programmes non synchronisés est dans le désordre dans JMM, mais l'ordre d'exécution des opérations vu par tous les threads peut également être incohérent. Par exemple, avant que le thread actuel ne mette en cache les données écrites dans la mémoire locale et ne les rafraîchisse dans la mémoire principale, l'opération d'écriture n'est visible que par le thread actuel du point de vue des autres threads, elle sera considérée comme l'écriture ; l’opération n’a pas eu lieu du tout. Exécutée par le thread actuel. Ce n'est qu'une fois que le thread actuel a vidé les données écrites dans la mémoire locale vers la mémoire principale que cette opération d'écriture peut être visible par les autres threads. Dans ce cas, l'ordre dans lequel les opérations sont effectuées sera incohérent entre le thread actuel et les autres threads.

Effet de cohérence séquentielle des programmes synchronisés

Ci-dessous, nous utilisons le moniteur pour synchroniser l'exemple de programme précédent, ReorderExample, pour voir comment un programme correctement synchronisé a une cohérence séquentielle.

Veuillez regarder l'exemple de code suivant :

class SynchronizedExample {
int a = 0;
boolean flag = false;

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

public synchronized void reader() {
    if (flag) {
        int i = a;
        ……
    }
}
}

Dans l'exemple de code ci-dessus, il est supposé qu'après que le thread A exécute la méthodewriter(), le thread B exécute le reader() méthode. Il s'agit d'un programme multithread correctement synchronisé. Selon la spécification JMM, les résultats d'exécution de ce programme seront les mêmes que les résultats d'exécution de ce programme dans le modèle de cohérence séquentielle. Ce qui suit est un tableau comparatif du timing d'exécution du programme dans les deux modèles de mémoire :

Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

Dans le modèle de cohérence séquentielle, toutes les opérations sont exécutées en série dans l'ordre du programme. Dans JMM, le code de la section critique peut être réorganisé (mais JMM ne permet pas au code de la section critique de « s'échapper » en dehors de la section critique, ce qui détruirait la sémantique du moniteur). JMM effectuera un traitement spécial aux deux moments clés de la sortie du moniteur et de l'entrée dans le moniteur, de sorte que le thread ait la même vue de mémoire que le modèle de cohérence séquentielle à ces deux moments (les détails spécifiques seront expliqués plus tard). Bien que le thread A ait été réorganisé dans la section critique, en raison des caractéristiques d'exécution mutuellement exclusives du moniteur, le thread B ne peut pas « observer » la réorganisation du thread A dans la section critique. Cette réorganisation améliore l'efficacité de l'exécution sans modifier les résultats d'exécution du programme.

De là, nous pouvons voir la politique de base de JMM dans une implémentation spécifique : sans modifier les résultats d'exécution du programme (correctement synchronisés), ouvrez autant de commodités que possible pour l'optimisation du compilateur et du processeur.

Caractéristiques d'exécution des programmes non synchronisés

Pour les programmes multi-thread non synchronisés ou mal synchronisés, JMM n'apporte qu'une sécurité minimale : la valeur lue lors de l'exécution du thread est soit la valeur d'un précédent La valeur écrite par chaque thread est soit la valeur par défaut (0, null, false). JMM garantit que la valeur lue par l'opération de lecture du thread n'apparaîtra pas de nulle part. Afin d'obtenir une sécurité minimale, lorsque la JVM alloue un objet sur le tas, elle va d'abord vider l'espace mémoire puis allouer l'objet dessus (la JVM synchronisera ces deux opérations en interne). Par conséquent, lorsque l'objet est alloué avec une mémoire pré-zéro, l'initialisation par défaut du domaine est déjà terminée.

JMM ne garantit pas que les résultats d'exécution d'un programme non synchronisé sont cohérents avec les résultats d'exécution du programme dans le modèle de cohérence séquentielle. Car lorsqu'un programme non synchronisé est exécuté dans le modèle de cohérence séquentielle, il est généralement dans le désordre et ses résultats d'exécution sont imprévisibles. Il ne sert à rien de garantir que les programmes non synchronisés s’exécutent avec des résultats cohérents dans les deux modèles.

Semblable au modèle de cohérence séquentielle, lorsqu'un programme non synchronisé est exécuté dans JMM, il est généralement dans le désordre et ses résultats d'exécution sont imprévisibles. Dans le même temps, les caractéristiques d'exécution des programmes non synchronisés dans ces deux modèles présentent les différences suivantes :
Le modèle de cohérence séquentielle garantit que les opérations au sein d'un même thread seront exécutées dans l'ordre du programme, tandis que JMM ne garantit pas que les opérations au sein d'un seul thread seront exécutées dans l'ordre du programme (comme la réorganisation ci-dessus du programme multithread correctement synchronisé dans la section critique). Ce sujet a déjà été évoqué et ne sera pas répété ici.
Le modèle de cohérence séquentielle garantit que tous les threads ne peuvent voir qu'un ordre cohérent d'exécution des opérations, tandis que JMM ne garantit pas que tous les threads peuvent voir un ordre cohérent d'exécution des opérations. Cela a déjà été évoqué et ne sera pas répété ici.
JMM ne garantit pas l'atomicité pour les opérations de lecture/écriture sur des variables doubles et de longueur 64 bits, tandis que le modèle de cohérence séquentielle garantit l'atomicité pour toutes les opérations de lecture/écriture en mémoire.

La troisième différence est étroitement liée au mécanisme de fonctionnement du bus du processeur. Dans un ordinateur, les données transitent entre le processeur et la mémoire via un bus. Chaque transfert de données entre le processeur et la mémoire s'effectue via une série d'étapes, appelées transactions de bus. Les transactions de bus incluent les transactions de lecture et les transactions d'écriture. Les transactions de lecture transfèrent les données de la mémoire vers le processeur et les transactions d'écriture transfèrent les données du processeur vers la mémoire. Chaque transaction lit/écrit un ou plusieurs mots physiquement consécutifs en mémoire. La clé ici est que le bus synchronise les transactions qui tentent d'utiliser le bus simultanément. Pendant qu'un processeur exécute une transaction de bus, le bus interdit à tous les autres processeurs et périphériques d'E/S de lire/écrire la mémoire. Illustrons le mécanisme de fonctionnement du bus à travers un diagramme schématique :

Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

Comme le montre la figure ci-dessus, supposons que les processeurs A, B et C lancent des transactions de bus vers le bus en même temps. À ce moment-là, l'arbitrage du bus statuera sur la concurrence. Ici, nous supposons que le bus détermine cela. le processeur A est en compétition après l'arbitrage Win (l'arbitrage de bus garantit que tous les processeurs ont un accès équitable à la mémoire). À ce moment, le processeur A continue sa transaction de bus, tandis que les deux autres processeurs doivent attendre la fin de la transaction de bus du processeur A avant de pouvoir recommencer à accéder à la mémoire. Supposons que pendant que le processeur A exécute une transaction de bus (que la transaction de bus soit une transaction de lecture ou une transaction d'écriture), le processeur D initie une transaction de bus vers le bus. À ce moment, la demande du processeur D sera interdite par le bus. .

Ces mécanismes de fonctionnement du bus peuvent effectuer l'accès de tous les processeurs à la mémoire de manière sérialisée à tout moment, un seul processeur peut accéder à la mémoire au maximum ; Cette fonctionnalité garantit que les opérations de lecture/écriture de mémoire au sein d’une seule transaction de bus sont atomiques.

Sur certains processeurs 32 bits, si l'opération de lecture/écriture de données 64 bits doit être atomique, il y aura une surcharge relativement importante. Afin de prendre en charge ce type de processeur, la spécification du langage Java encourage mais n'exige pas que la JVM ait une atomicité dans la lecture/écriture de variables de 64 bits et de variables doubles. Lorsque la JVM s'exécute sur un tel processeur, elle divisera l'opération de lecture/écriture d'une variable longue/double de 64 bits en deux opérations de lecture/écriture de 32 bits pour l'exécution. Ces deux opérations de lecture/écriture de 32 bits peuvent être affectées à différentes transactions de bus pour l'exécution. À ce stade, la lecture/écriture de cette variable de 64 bits ne sera pas atomique.

Lorsqu’une seule opération de mémoire n’est pas atomique, elle peut avoir des conséquences inattendues. Veuillez regarder le diagramme ci-dessous :

Analyse approfondie du modèle de mémoire Java : cohérence séquentielle

Comme le montre la figure ci-dessus, supposons que le processeur A écrit une variable longue et que le processeur B souhaite lire cette variable longue. L'opération d'écriture 64 bits dans le processeur A est divisée en deux opérations d'écriture 32 bits, et les deux opérations d'écriture 32 bits sont affectées à différentes transactions d'écriture pour exécution. Dans le même temps, l'opération de lecture de 64 bits dans le processeur B est divisée en deux opérations de lecture de 32 bits, et les deux opérations de lecture de 32 bits sont affectées à la même transaction de lecture pour exécution. Lorsque les processeurs A et B s'exécutent selon la séquence de synchronisation de la figure ci-dessus, le processeur B verra une valeur non valide qui n'est qu'à moitié écrite par le processeur A.

Ce qui précède est une analyse approfondie du modèle de mémoire Java : le contenu de la cohérence séquentielle. Pour plus de contenu connexe, veuillez faire attention au site Web PHP chinois (www.php.cn) !


Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn