Livrons d'abord une explication détaillée de Synchronized:
Synchronized est un mot-clé dans la langue java. Lorsqu'il est utilisé pour modifier une méthode ou un bloc de code, il peut s'assurer qu'au plus un thread exécute le code en même temps.
1. Lorsque deux threads simultanés accèdent à ce bloc de code synchronisé synchronisé (ce) dans le même objet d'objet, un seul thread peut être exécuté dans un temps. Un autre thread doit attendre que le thread actuel exécute ce bloc de code avant de pouvoir exécuter le bloc de code.
2. Cependant, lorsqu'un thread accède à un bloc de code de synchronisation synchronisé (this) d'un objet, un autre thread peut toujours accéder au bloc de code de synchronisation non synchronisé (this) dans cet objet.
3. Il est particulièrement essentiel que lorsqu'un thread accède à un bloc de code de synchronisation synchronisé (cette) de synchronisation d'un objet, d'autres threads seront empêchés d'accéder à tous les autres blocs de code de synchronisation synchronisés (this) dans l'objet.
4. Le troisième exemple s'applique également à d'autres blocs de code synchrones. Autrement dit, lorsqu'un thread accède à un bloc de code de synchronisation synchronisé (ce) synchronisation d'un objet, il obtient le verrouillage de l'objet de cet objet. En conséquence, d'autres threads accès à toutes les parties de code synchrones de l'objet objet sont temporairement bloqués.
5. Les règles ci-dessus s'appliquent également aux autres verrous d'objets.
En termes simples, Synchronisé déclare un verrou pour le fil actuel. Le fil avec ce verrou peut exécuter des instructions dans le bloc, et d'autres threads ne peuvent attendre que le verrou pour l'acquérir avant la même opération.
C'est très utile, mais j'ai rencontré une autre situation étrange.
1. Dans la même classe, il existe deux méthodes: en utilisant la déclaration de mots clés synchronisée
2. Lors de l'exécution de l'une des méthodes, vous devez attendre que l'autre méthode (rappel de threads asynchrones) soit exécutée, vous utilisez donc un compte à rebours pour attendre.
3. Le code est déconstruit comme suit:
synchronisé void a () {CountdownLatch = new CountdownLatch (1); // fait du compte à rebours.Await ();} void synchronisé b () {CountdownLatch.CountDown ();} dans
La méthode A est exécutée par le thread principal, la méthode B est exécutée par le thread asynchrone et le résultat de l'exécution du rappel est:
Le fil principal commence à rester coincé après avoir exécuté la méthode A, et ne le fait plus, et il sera inutile pour vous d'attendre, peu importe le temps qu'il faut.
Ceci est un problème de blocage classique
A attend que B s'exécute, mais en fait, ne pensez pas que B est un rappel, B attend également que A s'exécute. Pourquoi? Synchronisé joue un rôle.
D'une manière générale, lorsque nous voulons synchroniser un bloc de code, nous devons utiliser une variable partagée pour le verrouiller, par exemple:
BYTE [] MUTEX = NOUVEAU BYTE [0]; void A1 () {synchronisé (Mutex) {// Dosomething}} void b1 () {synchronisé (Mutex) {// Dosomething}} Si le contenu de la méthode A et B est migré vers les blocs synchronisés des méthodes A1 et B1 respectivement, il sera facile à comprendre.
Une fois A1 exécuté, il attendra indirectement la méthode (CountdownLatch) B1 à exécuter.
Cependant, comme le mutex dans A1 n'est pas libéré, nous commençons à attendre B1. À l'heure actuelle, même si la méthode Asynchrone Rallback B1 doit attendre que Mutex divulgue le verrou, la méthode B ne sera pas exécutée.
Cela a provoqué une impasse!
Le mot-clé synchronisé ici est placé devant la méthode, et la fonction est la même. C'est juste que la langue Java vous aide à cacher la déclaration et l'utilisation de Mutex. La méthode synchronisée utilisée dans le même objet est la même, donc même un rappel asynchrone provoquera des blocages, alors faites attention à ce problème. Ce niveau d'erreur est que le mot-clé synchronisé est mal utilisé. Ne l'utilisez pas au hasard et utilisez-le correctement.
Alors, quel est exactement un objet mutex aussi invisible?
L'exemple lui-même est facile à penser. Parce que de cette manière, il n'est pas nécessaire de définir un nouvel objet et de faire une serrure. Afin de prouver cette idée, vous pouvez écrire un programme pour le prouver.
L'idée est très simple. Définissez une classe et il existe deux méthodes. L'un est déclaré synchronisé, et l'autre est utilisé synchronisé (ceci) dans le corps de la méthode. Commencez ensuite deux threads pour appeler ces deux méthodes séparément. Si la compétition de verrouillage se produit entre les deux méthodes (attente), on peut expliquer que le mutex invisible en synchronisé déclaré par la méthode est en fait l'instance elle-même.
classe publique MultithReadSync {public synchronisé void m1 () lève InterruptedException {System. out.println ("M1 Call"); Fil de discussion. Sleep (2000); Système. out.println ("M1 Call Done"); } public void m2 () lève InterruptedException {synchronisé (this) {System. out.println ("M2 Call"); Fil de discussion. Sleep (2000); Système. out.println ("M2 Call Done"); }} public static void main (string [] args) {final multithreadSync thisObj = new MultithreadSync (); Thread t1 = new Thread () {@Override public void run () {try {thisObj.m1 (); } catch (InterruptedException e) {e.printStackTrace (); }}}}; Thread t2 = new Thread () {@Override public void run () {try {thisObj.m2 (); } catch (InterruptedException e) {e.printStackTrace (); }}}; t1.start (); t2.start (); }} La sortie du résultat est:
M1 Callm1 Appel Donem2 Callm2 Appel fait
Il est expliqué que le bloc de synchronisation de la méthode M2 attend l'exécution de M1. Cela peut confirmer le concept ci-dessus.
Il convient de noter que lorsque la synchronisation est ajoutée à la méthode statique, car il s'agit d'une méthode au niveau de la classe, l'objet verrouillé est l'instance de classe de la classe actuelle. Vous pouvez également écrire un programme pour le prouver. Ici, il est omis.
Par conséquent, le mot-clé synchronisé de la méthode peut être automatiquement remplacé par synchronisé (this) {} lors de la lecture, ce qui est facile à comprendre.
void méthode () {void synchronisé méthode () {synchronisé (this) {// code biz // code biz} ------ >>>}} Visibilité de la mémoire de synchronisée
En Java, nous savons tous que le mot-clé synchronisé peut être utilisé pour implémenter l'exclusion mutuelle entre les threads, mais nous oublions souvent qu'il a une autre fonction, c'est-à-dire pour garantir la visibilité des variables en mémoire - c'est-à-dire lorsque deux threads lisent et écrivent l'accès à la même variable en même temps, la dernière valeur de la variable de la variable, et la variable de la variable, et la variable peut être lue la variable.
Par exemple, l'exemple suivant:
classe publique Novissibilité {private static boolean ready = false; Numéro de statique statique privé = 0; classe statique privée ReaderThread étend Thread {@Override public void run () {while (! Ready) {thread.yield (); // prend en charge le CPU pour permettre aux autres threads de fonctionner} System.out.println (numéro); }} public static void main (string [] args) {new ReaderThread (). start (); nombre = 42; Ready = true; }}Que pensez-vous que la lecture des threads sortira? 42? Dans des circonstances normales, 42 seront sortis. Cependant, en raison de la réorganisation des problèmes, le thread de lecture peut sortir 0 ou sortir rien.
Nous savons que le compilateur peut réorganiser le code lors de la compilation du code Java en bytecode, et le CPU peut également réorganiser ses instructions lors de l'exécution des instructions de la machine. Tant que la réorganisation ne détruit pas la sémantique du programme
Dans un seul thread, tant que la réorganisation n'affecte pas le résultat d'exécution du programme, il ne peut pas être garanti que les opérations doivent être exécutées dans l'ordre spécifié par le programme, même si la réorganisation peut avoir un impact significatif sur d'autres threads.
Cela signifie que l'exécution de l'instruction "Ready = true" peut prendre priorité sur l'exécution de l'instruction "numéro = 42". Dans ce cas, le thread de lecture peut sortir la valeur par défaut du nombre 0.
Dans le cadre du modèle de mémoire Java, la réorganisation des problèmes entraînera de tels problèmes de visibilité de la mémoire. Dans le cadre du modèle de mémoire Java, chaque thread a sa propre mémoire de travail (principalement le cache ou le registre du CPU), et ses opérations sur des variables sont effectuées dans sa propre mémoire de travail, tandis que la communication entre les threads est réalisée par synchronisation entre la mémoire principale et la mémoire de travail du fil.
Par exemple, pour l'exemple ci-dessus, le thread d'écriture a réussi à mettre à jour le numéro à 42 et prêt à vrai, mais il est très probable que le thread d'écriture ne synchronise que le nombre à la mémoire principale (peut-être en raison du tampon d'écriture du CPU), ce qui entraîne la valeur prête lue par les threads de lecture suivants toujours faux, donc le code ci-dessus ne publiera aucune valeur numérique.
Si nous utilisons le mot-clé synchronisé pour synchroniser, il n'y aura pas de tel problème.
classe publique Novissibilité {private static boolean ready = false; Numéro de statique statique privé = 0; Lock d'objet statique privé = nouveau objet (); classe statique privée ReaderThread étend Thread {@Override public void run () {synchronisé (lock) {while (! ready) {Thread.yield (); } System.out.println (numéro); }} public static void main (string [] args) {synchronisé (lock) {new ReaderThread (). start (); nombre = 42; Ready = true; }}} En effet, le modèle de mémoire Java fournit les garanties suivantes pour la sémantique synchronisée.
Autrement dit, lorsque Threada libère le verrouillage M, les variables qu'il a écrites (telles que X et Y, qui sont présentes dans sa mémoire de travail) seront synchronisées dans la mémoire principale. Lorsque ThreadB s'applique pour le même verrouillage M, la mémoire de travail de ThreadB sera définie sur invalide, puis ThreadB rechargera la variable à laquelle il souhaite accéder à partir de la mémoire principale dans sa mémoire de travail (pour le moment, x = 1, y = 1, est la dernière valeur modifiée dans Threada). De cette façon, la communication entre les threads de Threada à Threadb est réalisée.
Il s'agit en fait de l'une des règles qui se déroulent avantage définies par JSR133. JSR133 définit l'ensemble suivant de règles en cours pour le modèle de mémoire Java.
En fait, cet ensemble de règles se produit avant définit la visibilité de la mémoire entre les opérations. Si un fonctionnement du fonctionnement est avant B, le résultat d'exécution d'une opération (comme l'écriture aux variables) doit être visible lors de l'exécution de l'opération B.
Pour acquérir une compréhension plus approfondie de ces règles qui se produisent avant, prenons un exemple:
// Code partagé par Thread A et B Object Lock = New Object (); int a = 0; int b = 0; int c = 0; // thread a, appelez le code suivant synchronisé (Lock) {a = 1; // 1 b = 2; // 2} // 3c = 3; // 4 // Thread B, appelez le code suivant synchronisé (Lock) {// 5 System.out.println (a); // 6 System.out.println (b); // 7 System.out.println (C); // 8}Nous supposons que le thread A s'exécute d'abord, attribue respectivement des valeurs aux trois variables A, B et C (Remarque: L'attribution des variables A, B est effectuée dans le bloc de déclaration synchrone), puis le thread B fonctionne à nouveau, lisant les valeurs de ces trois variables et les imprimant. Alors, quelles sont les valeurs des variables A, B et C imprimées par le fil B?
Selon la règle de lancement unique, dans l'exécution du thread A, nous pouvons obtenir que 1 opération se produit avant 2 opérations, 2 L'opération se produit avant 3 opérations et 3 opération se produit avant 4 opérations. De même, dans l'exécution du thread B, 5 opérations se produisent avant 6 opérations, 6 opérations se produisent avant 7 opérations et 7 opérations se produisent avant 8 opérations. Selon les principes de déverrouillage et de verrouillage du moniteur, les 3 opérations (opération de déverrouillage) se produisent avant 5 opérations (opération de verrouillage). Selon les règles transitives, nous pouvons conclure que les opérations 1 et 2 se produisent avant les opérations 6, 7 et 8.
Selon la sémantique de mémoire de se passer avant, les résultats d'exécution des opérations 1 et 2 sont visibles pour les opérations 6, 7 et 8, donc dans le thread B, A et B sont imprimés doivent être 1 et 2. Pour les opérations 4 et l'opération 8 de la variable c. Nous ne pouvons pas déduire l'opération 4 se produit avant l'opération 8 Selon les règles existantes se produisent avant les règles. Par conséquent, dans le thread B, la variable accessible à C peut toujours être 0, pas 3.