Les variables en Java sont divisées en deux catégories : les variables locales et les variables de classe. Les variables locales font référence aux variables définies dans une méthode, telles que les variables définies dans la méthode run. Pour ces variables, il n’y a aucun problème de partage entre threads. Ils ne nécessitent donc pas de synchronisation des données. Les variables de classe sont des variables définies dans une classe et leur portée s'étend à l'ensemble de la classe. Ces variables peuvent être partagées par plusieurs threads. Par conséquent, nous devons effectuer une synchronisation des données sur ces variables.
La synchronisation des données signifie qu'un seul thread peut accéder aux variables de classe synchronisées en même temps. Une fois que le thread actuel a accédé à ces variables, les autres threads peuvent continuer à y accéder. L'accès mentionné ici fait référence à l'accès avec des opérations d'écriture. Si tous les threads accédant aux variables de classe sont des opérations de lecture, la synchronisation des données n'est généralement pas requise. Alors, que se passe-t-il si la synchronisation des données n'est pas effectuée sur les variables de classe partagées ? Voyons d'abord ce qui se passe avec le code suivant :
Copiez le code comme suit :
test de paquet ;
la classe publique MyThread étend Thread
{
public statique int n = 0 ;
exécution publique vide()
{
int m = n;
rendement();
m++;
n = m;
}
public static void main (String[] args) lève une exception
{
MonThread monThread = nouveau MonThread ();
Fils de discussion[] = nouveau fil[100] ;
pour (int i = 0; i < threads.length; i++)
threads[i] = nouveau Thread(monThread);
pour (int i = 0; i < threads.length; i++)
fils[i].start();
pour (int i = 0; i < threads.length; i++)
fils de discussion[i].join();
System.out.println("n = " + MonThread.n);
}
}
Les résultats possibles de l'exécution du code ci-dessus sont les suivants :
Copiez le code comme suit :
n = 59
De nombreux lecteurs pourraient être surpris de voir ce résultat. Ce programme démarre évidemment 100 threads, puis chaque thread incrémente la variable statique n de 1. Enfin, utilisez la méthode join pour exécuter les 100 threads, puis affichez la valeur n. Normalement, le résultat devrait être n = 100. Mais le résultat est inférieur à 100.
En fait, la cause de ce résultat sont les « données sales » dont nous parlons souvent. L'instruction rendement() dans la méthode run est l'initiatrice des « données sales » (sans ajouter d'instruction rendement, des « données sales » peuvent également être générées, mais cela ne sera pas si évident. Ce n'est qu'en remplaçant 100 par un nombre plus grand que cela se produit souvent. Générer des "données sales", appeler rendement dans cet exemple consiste à amplifier l'effet des "données sales"). La fonction de la méthode Yield est de mettre le thread en pause, c'est-à-dire de faire en sorte que le thread appelant la méthode Yield abandonne temporairement les ressources du processeur, donnant ainsi au processeur une chance d'exécuter d'autres threads. Pour illustrer comment ce programme génère des « données sales », supposons que seuls deux threads soient créés : thread1 et thread2. Puisque la méthode start de thread1 est appelée en premier, la méthode run de thread1 s'exécutera généralement en premier. Lorsque la méthode run de thread1 s'exécute sur la première ligne (int m = n;), la valeur de n est affectée à m. Lorsque la méthode de rendement de la deuxième ligne est exécutée, thread1 cessera temporairement de s'exécuter. Lorsque thread1 est en pause, thread2 commence à s'exécuter après avoir obtenu les ressources CPU (thread2 était auparavant dans l'état prêt lorsque thread2 exécute la première ligne (int When). m = n;), puisque n est toujours 0 lorsque thread1 est exécuté pour céder, la valeur obtenue par m dans thread2 est également 0. Cela fait que les valeurs m de thread1 et thread2 obtiennent toutes deux 0. Après avoir exécuté la méthode rendement, ils partent tous de 0 et ajoutent 1. Par conséquent, peu importe qui l'exécute en premier, la valeur finale de n est 1, mais ce n se voit attribuer une valeur par thread1 et thread2 respectivement. Quelqu'un peut demander, s'il n'y a que n++, des « données sales » seront-elles générées ? La réponse est oui. Donc n++ n'est qu'une instruction, alors comment confier le CPU à d'autres threads pendant l'exécution ? En fait, ce n'est qu'un phénomène superficiel. Une fois n++ compilé dans un langage intermédiaire (également appelé bytecode) par le compilateur Java, ce n'est plus un langage. Voyons dans quel langage intermédiaire Java le code Java suivant sera compilé.
Copiez le code comme suit :
exécution publique vide()
{
n++;
}
Code de langue intermédiaire compilé
Copiez le code comme suit :
exécution publique vide()
{
aload_0
dup
getfield
icônest_1
j'ajoute
champ de pute
retour
}
Vous pouvez voir qu'il n'y a qu'une instruction n++ dans la méthode run, mais après compilation, il y a 7 instructions de langage intermédiaires. Nous n'avons pas besoin de savoir quelles sont les fonctions de ces relevés, il suffit de regarder les relevés des lignes 005, 007 et 008. La ligne 005 est getfield. Selon sa signification anglaise, nous savons que nous voulons obtenir une certaine valeur. Parce qu'il n'y a qu'un seul n ici, il ne fait aucun doute que nous voulons obtenir la valeur de n. Il n'est pas difficile de deviner que l'iadd de la ligne 007 consiste à ajouter 1 à la valeur n obtenue. Je pense que vous avez peut-être deviné la signification de putfield à la ligne 008. Il est responsable de la mise à jour du n après avoir rajouté 1 à la variable de classe n. En parlant de ça, vous avez peut-être encore un doute. Lors de l'exécution de n++, il suffit d'ajouter n par 1. Pourquoi cela prend-il autant de mal ? En fait, cela implique un problème de modèle de mémoire Java.
Le modèle de mémoire de Java est divisé en zone de stockage principale et zone de stockage de travail. La zone de stockage principale stocke toutes les instances en Java. C'est-à-dire qu'après avoir utilisé new pour créer un objet, l'objet et ses méthodes internes, variables, etc. sont enregistrés dans cette zone, et n dans la classe MyThread est enregistré dans cette zone. Le stockage principal peut être partagé par tous les threads. La zone de stockage de travail est la pile de threads dont nous avons parlé plus tôt. Dans cette zone, les variables définies dans la méthode run et la méthode appelée par la méthode run sont stockées, c'est-à-dire les variables de méthode. Lorsqu'un thread souhaite modifier des variables dans la zone de stockage principale, il ne modifie pas ces variables directement, mais les copie dans la zone de stockage de travail du thread actuel. Une fois la modification terminée, la valeur de la variable est écrasée par la valeur correspondante. valeur dans la zone de stockage principale.
Après avoir compris le modèle de mémoire de Java, il n'est pas difficile de comprendre pourquoi n++ n'est pas une opération atomique. Il doit passer par un processus de copie, d'ajout de 1 et d'écrasement. Ce processus est similaire à celui simulé dans la classe MyThread. Comme vous pouvez l'imaginer, si le thread1 est interrompu pour une raison quelconque lors de l'exécution de getfield, une situation similaire au résultat de l'exécution de la classe MyThread se produira. Pour résoudre complètement ce problème, nous devons utiliser une méthode pour synchroniser n, c'est-à-dire qu'un seul thread peut opérer n en même temps, ce qui est également appelé opération atomique sur n.