Perdu deux cheveux, il peut être considéré comme volatil

本来想着快过年了偷个懒休息下,没想到被兄弟们连续催更,没办法,博主暖男嘛,掐着人中也要更,兄弟们卷起来

Le mot-clé volatile peut être considéré comme le mécanisme de synchronisation le plus léger fourni par la machine virtuelle Java, mais quant à la raison pour laquelle il ne peut garantir que la visibilité, pas l'atomicité, et comment il désactive le réarrangement des instructions, il y a encore de nombreux étudiants qui n'ont pas bien compris

Croyez-moi, continuez à lire cet article, vous saisirez fermement un point de connaissance de base de Java

Parlons d'abord de ses deux fonctions :

  • Visibilité garantie des variables en mémoire pour les threads
  • Désactiver la réorganisation des instructions

Je connais chaque mot, c'est engourdi une fois assemblé

Ces deux fonctions ne sont généralement pas faciles à comprendre correctement et complètement pour nous, les développeurs Java, de sorte que de nombreux étudiants ne peuvent pas utiliser volatile correctement.

À propos de la visibilité

Pas beaucoup bb, code ici

public class VolatileTest {
    
    
    private static volatile int count = 0;

    private static void increase() {
    
    
        count++;
    }

    public static void main(String[] args) throws InterruptedException {
    
    
        for (int i = 0; i < 10; i++) {
    
    
            new Thread(() -> {
    
    
                for (int j = 0; j < 10000; j++) {
    
    
                    increase();
                }
            }).start();
        }
		// 所有线程累加完成后输出
        while (Thread.activeCount() > 2) Thread.yield();
        System.out.println(count);
    }
}

Le code est facile à comprendre. Dix threads sont ouverts pour accumuler le même nombre de variables partagées, et chaque thread accumule 1w fois.

Nous avons décoré count avec volatile, ce qui a garanti la visibilité de count à dix threads en mémoire. Il va de soi que la valeur de count devrait être de 10w après l'exécution de dix threads.

Cependant, après plusieurs exécutions, les résultats sont bien inférieurs à la valeur attendue
insérez la description de l'image ici
. Quel lien pose problème ?
insérez la description de l'image ici

Vous avez dû entendre une phrase : volatile ne garantit que la visibilité, pas l'atomicité

Cette phrase est la réponse, mais beaucoup de gens ne comprennent toujours pas le mystère

C'est une longue histoire et je vais la faire courte Bref, l'opération count++ n'est pas atomique, elle s'effectue en trois étapes.

  1. Lire la valeur de count à partir de la mémoire
  2. nombre d'exécutions + 1
  3. écrire la nouvelle valeur de count back

Pour bien comprendre ce problème, nous devons commencer par le bytecode

Voici le bytecode compilé par la méthode d'augmentation.
insérez la description de l'image ici
Peu importe si vous ne le comprenez pas, regardons-le ligne par ligne :

  1. GETSTATIC : lire la valeur actuelle de count
  2. ICONST_1 : charge la constante 1 en haut de la pile
  3. IADD : +1 pour la mise en œuvre
  4. PUTSTATIC : écrivez la dernière valeur de count

ICONST_1 et IADD sont en fait de vraies opérations ++

Le point clé est que volatile ne peut que garantir que la valeur obtenue par le thread dans l'étape GETSTATIC est la plus récente, mais lorsque le thread exécute les lignes d'instructions suivantes, d'autres threads peuvent modifier la valeur de count pendant cette période, ce qui finira par conduire à L'ancienne valeur écrase la vraie nouvelle valeur

comprend moi

Par conséquent, en programmation concurrente, il n'est pas fiable de s'appuyer uniquement sur volatile pour modifier les variables partagées.En fin de compte, il est nécessaire de verrouiller les méthodes clés pour assurer la sécurité des threads.

Tout comme la démo ci-dessus, une petite modification peut atteindre une véritable sécurité des threads

Le plus simple est d'ajouter un synchronized à la méthode d'augmentation (je ne serai pas prolixe sur la façon dont synchronized atteint la sécurité des threads. J'ai déjà parlé du principe de mise en œuvre sous-jacent de synchronized avant )

    private synchronized static void increase() {
    
    
        ++count;
    }

Exécutez-le plusieurs fois,
insérez la description de l'image ici
n'est-ce pas correct ?

À présent, vous devriez avoir une nouvelle compréhension des deux points suivants

  • volatile garantit la visibilité de la variable en mémoire au thread
  • volatile ne garantit que la visibilité, pas l'atomicité

À propos de la réorganisation des instructions

En programmation concurrente, afin d'améliorer l'efficacité d'exécution, le CPU lui-même et la machine virtuelle utiliseront le réarrangement d'instructions (sous la prémisse de s'assurer que les résultats ne sont pas affectés, certains codes sont exécutés dans le désordre)

  • À propos du processeur : afin d'utiliser le processeur, l'exécution réelle des instructions sera optimisée ;
  • À propos de la machine virtuelle : dans HotSpot vm, afin d'améliorer l'efficacité de l'exécution, le mode JIT (compilation juste-à-temps) effectuera également l'optimisation des instructions

Le réarrangement des instructions peut en effet améliorer l'efficacité de l'exécution dans la plupart des scénarios, mais certains scénarios dépendent fortement de l'ordre d'exécution du code. Dans ce cas, nous devons désactiver le réarrangement des instructions. Le
insérez la description de l'image ici
pseudocode du scénario suivant est tiré de "In-depth Understanding of Machine virtuelle Java" :

Le scénario décrit est un processus de lecture de configuration courant dans le développement, mais nous ne rencontrons généralement pas de simultanéité lors du traitement des fichiers de configuration, nous n'avons donc pas réalisé que cela poserait un problème.
Imaginez, si la modification volatile n'est pas utilisée lors de la définition de la variable initialisée, le dernier code "initialized=true" dans le thread A peut être exécuté à l'avance en raison de l'optimisation de la réorganisation des instructions (bien que Java soit utilisé comme pseudo-code ici, tout la référence à l'optimisation de la réorganisation est une opération d'optimisation au niveau de la machine, et une exécution anticipée signifie que le code assembleur correspondant à cette instruction est exécuté à l'avance), de sorte que le code utilisant les informations de configuration dans le thread B peut avoir des erreurs, et volatile interdit l'instruction un réarrangement de votre part peut éviter que cela ne se produise

Désactiver le réarrangement des instructions ne nécessite que de déclarer la variable comme volatile, n'est-ce pas magique ?

Voyons comment la volatilité désactive la réorganisation des instructions

Empruntons également un exemple à "Understanding Java Virtual Machine", il est plus facile à comprendre.
insérez la description de l'image ici
Il s'agit de l'implémentation du mode singleton. Ce qui suit fait partie de son bytecode. La case rouge mov%eax, 0x150(%esi) est le affectation à l'instance
insérez la description de l'image ici
On peut voir qu'après l'affectation, l' instruction de verrouillage addl$0x0, (%esp) est également exécutée. Le point clé est ici. Cette ligne d'instruction équivaut à définir une barrière mémoire . Après la barrière mémoire , le cpu ou virtuel La machine ne peut pas faire avancer les instructions derrière la barrière mémoire vers l'avant de la barrière mémoire lorsque les instructions sont réarrangées.Regardez bien ce passage.


Enfin, laissez une question qui peut approfondir la compréhension de chacun du volatil. Frères, réfléchissez-y :

Le code Java est évidemment exécuté de haut en bas, pourquoi le problème de réarrangement des instructions se pose-t-il ?

C'est bon, j'ai fini

Je suppose que tu aimes

Origine blog.csdn.net/qq_33709582/article/details/122415754
conseillé
Classement