Maison > Java > javaDidacticiel > le corps du texte

Explication détaillée des problèmes de blocage et d'utilisation de la mémoire du mot-clé synchronisé en Java

高洛峰
Libérer: 2017-01-05 16:10:34
original
1977 Les gens l'ont consulté

Regardons d'abord une explication détaillée de synchronisé :
synchronized est un mot-clé du langage Java Lorsqu'il est utilisé pour modifier une méthode ou un bloc de code, il peut garantir qu'au plus un thread exécute le code à un moment donné. en même temps.

1. Lorsque deux threads simultanés accèdent au bloc de code de synchronisation synchronisé (ce) dans le même objet, un seul thread peut être exécuté à la fois. Un autre thread doit attendre que le thread actuel ait fini d'exécuter ce bloc de code avant de pouvoir exécuter ce bloc de code.

2. Cependant, lorsqu'un thread accède à un bloc de code synchronisé (this) d'un objet, un autre thread peut toujours accéder au bloc de code synchronisé non synchronisé (this) dans l'objet.

3. Ce qui est particulièrement critique est que lorsqu'un thread accède à un bloc de code synchronisé (this) d'un objet, les autres threads ne pourront pas accéder à tous les autres blocs de code synchronisés (this) dans l'objet.

4. Le troisième exemple est également applicable à d'autres blocs de codes de synchronisation. C'est-à-dire que lorsqu'un thread accède à un bloc de code synchronisé d'un objet, il obtient le verrou d'objet de cet objet. En conséquence, l'accès des autres threads à toutes les parties de code synchronisées de l'objet objet est temporairement bloqué.

5. Les règles ci-dessus sont également applicables à d'autres verrous d'objets
En termes simples, synchronisé consiste à déclarer un verrou pour le thread actuel. Le thread qui possède ce verrou peut exécuter les instructions dans le bloc, et autres threads Vous ne pouvez qu'attendre d'acquérir le verrou, puis effectuer la même opération.
C'est très utile, mais l'auteur a rencontré une autre situation étrange.
Dans la même classe, il existe deux méthodes qui. utilisez la déclaration de mot-clé synchronisée
2. Lorsqu'une des méthodes est exécutée, elle doit attendre que l'autre méthode (rappel de thread asynchrone) soit exécutée, donc un countDownLatch est utilisé pour attendre
3. comme suit :

synchronized void a(){
 countDownLatch = new CountDownLatch(1);
 // do someing
 countDownLatch.await();
}
 
synchronized void b(){
   countDownLatch.countDown();
}
Copier après la connexion

Parmi elles,
la méthode a est exécutée par le thread principal, la méthode b est exécutée par le thread asynchrone puis rappelée
Le résultat de l'exécution est : <🎜. > Le thread principal commence à rester bloqué après l'exécution de la méthode a et ne continuera plus peu importe le temps que vous attendez.
Il s'agit d'un problème de blocage très classique
a attend que b s'exécute. en fait, ne considérez pas b comme un rappel, b attend également que a soit exécuté. Pourquoi ? De manière générale, lorsque nous voulons synchroniser un bloc de code, nous devons utiliser une variable partagée pour le verrouiller. , tel que :

Si le contenu de la méthode a et de la méthode b est séparé La migration vers le bloc synchronisé des méthodes a1 et b1 est facile à comprendre
byte[] mutex = new byte[0];
 
void a1(){
   synchronized(mutex){
     //dosomething
   }
}
 
void b1(){
 
   synchronized(mutex){
     // dosomething
   }
 
}
Copier après la connexion
Une fois a1 exécuté, ce sera le cas. attendez indirectement (countDownLatch) que la méthode b1 s'exécute.

Cependant, comme le mutex dans a1 n'a pas été libéré, il commence à attendre b1 à ce moment, même s'il s'agit d'un rappel asynchrone à la méthode b1, puisque. il doit attendre que le mutex libère le verrou, la méthode b ne sera pas exécutée.
Cela provoque une impasse !
Le mot-clé synchronisé ici a le même effet lorsqu'il est placé devant la méthode. C'est juste que le langage Java vous aide à masquer la déclaration et l'utilisation du mutex. Le mutex utilisé par la méthode synchronisée dans le même objet est le même. donc même les rappels asynchrones peuvent provoquer des blocages, alors faites attention à ce problème. Ce niveau d'erreur est dû à une utilisation inappropriée du mot-clé synchronisé, et utilisez-le correctement
Ensuite, un tel objet mutex invisible. De quoi s'agit-il exactement ?
Il est facile de penser à l'instance elle-même, car de cette façon, vous n'avez pas besoin de définir un nouvel objet et de le verrouiller. Afin de prouver cette idée, vous pouvez écrire un programme pour la prouver.
L'idée est très simple, définir Une classe a deux méthodes, une méthode est déclarée synchronisée, on utilise synchronisé(this) dans le corps de la méthode, puis démarre deux threads pour appeler ces deux méthodes respectivement en cas de compétition de verrouillage. se produit entre les deux méthodes (wait ), on peut expliquer que le mutex invisible dans la déclaration de méthode synchronisée est en fait l'instance elle-même

Le résultat est :
public class MultiThreadSync {
 
  public synchronized void m1() throws InterruptedException{
     System. out.println("m1 call" );
     Thread. sleep(2000);
     System. out.println("m1 call done" );
  }
 
  public void m2() throws InterruptedException{
     synchronized (this ) {
       System. out.println("m2 call" );
       Thread. sleep(2000);
       System. 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();
 
  }
 
}
Copier après la connexion

<🎜. >Explication selon laquelle le bloc sync de la méthode m2 attend De cette façon, l'hypothèse ci-dessus peut être confirmée

Il convient également de noter que lorsque sync est ajouté à une méthode statique, puisqu'il s'agit d'une méthode au niveau de la classe, le verrouillé. L'objet est de l'exemple de classe actuel. Vous pouvez également écrire un programme pour le prouver. Ceci est omis ici.
m1 call
m1 call done
m2 call
m2 call done
Copier après la connexion
Ainsi, le mot-clé synchronisé de la méthode peut être automatiquement remplacé par synchronisé(this){}, ce qui est facile à comprendre.



Commençons par la visibilité mémoire de Synchronized

En Java, nous savons tous que le mot-clé synchronisé peut être utilisé pour implémenter l'exclusion mutuelle entre les threads, mais on oublie souvent que il a une autre fonction, c'est-à-dire Assurer la visibilité des variables en mémoire - c'est-à-dire que lorsque deux threads, en lecture et en écriture, accèdent à la même variable en même temps, synchronisé est utilisé pour garantir qu'après que le thread d'écriture ait mis à jour la variable, le le thread de lecture peut lire la dernière valeur de la variable lorsqu'il accède à nouveau à la variable.
                    void method(){
void synchronized method(){         synchronized(this){
   // biz code                // biz code
}               ------>>>   }
                    }
Copier après la connexion

Par exemple, l'exemple suivant :

Que pensez-vous que le fil de lecture va produire ? 42 ? Dans des circonstances normales, 42 sera affiché. Cependant, en raison de problèmes de réorganisation, le thread de lecture peut afficher 0 ou rien.

public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      while (!ready) {
        Thread.yield(); //交出CPU让其它线程工作
      }
      System.out.println(number);
    }
  }
 
  public static void main(String[] args) {
    new ReaderThread().start();
    number = 42;
    ready = true;
  }
}
Copier après la connexion
Nous savons que le compilateur peut réorganiser le code lors de la compilation du code Java en bytecode, et que le CPU peut également réorganiser ses instructions lors de l'exécution des instructions machine. Le tri ne rompt pas la sémantique du programme -

.

在单一线程中,只要重排序不会影响到程序的执行结果,那么就不能保证其中的操作一定按照程序写定的顺序执行,即使重排序可能会对其它线程产生明显的影响。
这也就是说,语句"ready=true"的执行有可能要优先于语句"number=42"的执行,这种情况下,读线程就有可能会输出number的默认值0.

而在Java内存模型下,重排序问题是会导致这样的内存的可见性问题的。在Java内存模型下,每个线程都有它自己的工作内存(主要是CPU的cache或寄存器),它对变量的操作都在自己的工作内存中进行,而线程之间的通信则是通过主存和线程的工作内存之间的同步来实现的。

比如说,对于上面的例子而言,写线程已经成功的将number更新为42,ready更新为true了,但是很有可能写线程只同步了number到主存中(可能是由于CPU的写缓冲导致),导致后续的读线程读取的ready值一直为false,那么上面的代码就不会输出任何数值。

而如果我们使用了synchronized关键字来进行同步,则不会存在这样的问题,

public class NoVisibility {
  private static boolean ready = false;
  private static int number = 0;
  private static Object lock = new Object();
 
  private static class ReaderThread extends Thread {
    @Override
    public void run() {
      synchronized (lock) {
        while (!ready) {
          Thread.yield();
        }
        System.out.println(number);
      }
    }
  }
 
  public static void main(String[] args) {
    synchronized (lock) {
      new ReaderThread().start();
      number = 42;
      ready = true;
    }
  }
}
Copier après la connexion

这个是因为Java内存模型对synchronized语义做了以下的保证,

即当ThreadA释放锁M时,它所写过的变量(比如,x和y,存在它工作内存中的)都会同步到主存中,而当ThreadB在申请同一个锁M时,ThreadB的工作内存会被设置为无效,然后ThreadB会重新从主存中加载它要访问的变量到它的工作内存中(这时x=1,y=1,是ThreadA中修改过的最新的值)。通过这样的方式来实现ThreadA到ThreadB的线程间的通信。

这实际上是JSR133定义的其中一条happen-before规则。JSR133给Java内存模型定义以下一组happen-before规则,

单线程规则:同一个线程中的每个操作都happens-before于出现在其后的任何一个操作。

对一个监视器的解锁操作happens-before于每一个后续对同一个监视器的加锁操作。

对volatile字段的写入操作happens-before于每一个后续的对同一个volatile字段的读操作。

Thread.start()的调用操作会happens-before于启动线程里面的操作。

一个线程中的所有操作都happens-before于其他线程成功返回在该线程上的join()调用后的所有操作。

一个对象构造函数的结束操作happens-before与该对象的finalizer的开始操作。

传递性规则:如果A操作happens-before于B操作,而B操作happens-before与C操作,那么A动作happens-before于C操作。

实际上这组happens-before规则定义了操作之间的内存可见性,如果A操作happens-before B操作,那么A操作的执行结果(比如对变量的写入)必定在执行B操作时可见。

为了更加深入的了解这些happens-before规则,我们来看一个例子:

//线程A,B共同访问的代码
Object lock = new Object();
int a=0;
int b=0;
int c=0;
 
//线程A,调用如下代码
synchronized(lock){
  a=1; //1
  b=2; //2
} //3
c=3; //4
 
 
//线程B,调用如下代码
synchronized(lock){ //5
  System.out.println(a); //6
  System.out.println(b); //7
  System.out.println(c); //8
}
Copier après la connexion

我们假设线程A先运行,分别给a,b,c三个变量进行赋值(注:变量a,b的赋值是在同步语句块中进行的),然后线程B再运行,分别读取出这三个变量的值并打印出来。那么线程B打印出来的变量a,b,c的值分别是多少?

根据单线程规则,在A线程的执行中,我们可以得出1操作happens before于2操作,2操作happens before于3操作,3操作happens before于4操作。同理,在B线程的执行中,5操作happens before于6操作,6操作happens before于7操作,7操作happens before于8操作。而根据监视器的解锁和加锁原则,3操作(解锁操作)是happens before 5操作的(加锁操作),再根据传递性 规则我们可以得出,操作1,2是happens before 操作6,7,8的。

则根据happens-before的内存语义,操作1,2的执行结果对于操作6,7,8是可见的,那么线程B里,打印的a,b肯定是1和2. 而对于变量c的操作4,和操作8. 我们并不能根据现有的happens before规则推出操作4 happens before于操作8. 所以在线程B中,访问的到c变量有可能还是0,而不是3.


更多详解Java中synchronized关键字的死锁和内存占用问题相关文章请关注PHP中文网!


Étiquettes associées:
source:php.cn
Déclaration de ce site Web
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
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal