Heim  >  Artikel  >  Java  >  Detaillierte Erläuterung von Deadlock- und Speichernutzungsproblemen des synchronisierten Schlüsselworts in Java

Detaillierte Erläuterung von Deadlock- und Speichernutzungsproblemen des synchronisierten Schlüsselworts in Java

高洛峰
高洛峰Original
2017-01-05 16:10:341972Durchsuche

Sehen wir uns zunächst eine detaillierte Erklärung von synchronisiert an:
synchronisiert ist ein Schlüsselwort in der Java-Sprache. Wenn es zum Ändern einer Methode oder eines Codeblocks verwendet wird, kann es sicherstellen, dass höchstens ein Thread den Code ausführt zur gleichen Zeit.

1. Wenn zwei gleichzeitige Threads auf den synchronisierten (diesen) Synchronisationscodeblock im selben Objekt zugreifen, kann jeweils nur ein Thread ausgeführt werden. Ein anderer Thread muss warten, bis der aktuelle Thread die Ausführung dieses Codeblocks abgeschlossen hat, bevor er diesen Codeblock ausführen kann.

2. Wenn jedoch ein Thread auf einen synchronisierten (diesen) synchronisierten Codeblock des Objekts zugreift, kann ein anderer Thread weiterhin auf den nicht synchronisierten (diesen) synchronisierten Codeblock im Objekt zugreifen.

3. Was besonders kritisch ist, ist, dass, wenn ein Thread auf einen synchronisierten (diesen) synchronisierten Codeblock des Objekts zugreift, andere Threads daran gehindert werden, auf alle anderen synchronisierten (diesen) synchronisierten Codeblöcke im Objekt zuzugreifen.

4. Das dritte Beispiel ist auch auf andere Synchronisationscodeblöcke anwendbar. Das heißt, wenn ein Thread auf einen synchronisierten (diesen) synchronisierten Codeblock eines Objekts zugreift, erhält er die Objektsperre dieses Objekts. Dadurch wird der Zugriff anderer Threads auf alle synchronisierten Codeteile des Objektobjekts vorübergehend blockiert.

5. Die oben genannten Regeln gelten auch für andere Objektsperren.
Synchronisiert bedeutet einfach, eine Sperre für den aktuellen Thread zu deklarieren, der die Anweisungen im Block ausführen kann. und andere Threads Sie können nur darauf warten, die Sperre zu erhalten, und dann den gleichen Vorgang ausführen.
Das ist sehr nützlich, aber der Autor ist auf eine andere seltsame Situation gestoßen.
1 In derselben Klasse gibt es zwei Methoden Verwenden Sie eine synchronisierte Schlüsselwortdeklaration
2. Wenn eine der Methoden ausgeführt wird, muss sie auf die Ausführung der anderen Methode (asynchroner Thread-Rückruf) warten, daher wird ein countDownLatch verwendet, um zu warten
3 Der Code wird dekonstruiert wie folgt:

synchronized void a(){
 countDownLatch = new CountDownLatch(1);
 // do someing
 countDownLatch.await();
}
 
synchronized void b(){
   countDownLatch.countDown();
}

Unter diesen wird
Methode a vom Hauptthread ausgeführt, Methode b wird vom asynchronen Thread ausgeführt und dann zurückgerufen
Das Ausführungsergebnis ist:
Der Hauptthread bleibt nach der Ausführung von Methode a hängen und wird nicht mehr fortgesetzt.
Dies ist ein sehr klassisches Deadlock-Problem.
a wartet auf die Ausführung von b Betrachten Sie b nicht als Rückruf, b wartet auch auf die Ausführung von a.
Wenn wir einen Codeblock synchronisieren möchten, müssen wir ihn im Allgemeinen sperren , wie zum Beispiel:

byte[] mutex = new byte[0];
 
void a1(){
   synchronized(mutex){
     //dosomething
   }
}
 
void b1(){
 
   synchronized(mutex){
     // dosomething
   }
 
}

Wenn der Inhalt von Methode a und Methode b getrennt ist, ist die Migration zum synchronisierten Block der Methoden a1 und b1 leicht zu verstehen.
Nachdem a1 ausgeführt wurde, wird dies der Fall sein Warten Sie indirekt (countDownLatch), bis die b1-Methode ausgeführt wird. Da der Mutex in a1 jedoch nicht freigegeben wurde, beginnt er zu diesem Zeitpunkt zu warten, auch wenn die asynchrone Rückrufmethode b1 warten muss Um die Sperre aufzuheben, wird Methode b nicht ausgeführt.
Dies führt zu einem Deadlock!
Das synchronisierte Schlüsselwort hat hier den gleichen Effekt, wenn es vor der Methode platziert wird. Die Java-Sprache hilft Ihnen jedoch dabei, die Deklaration und Verwendung von Mutex zu verbergen. Der von der synchronisierten Methode im selben Objekt verwendete Mutex ist derselbe. Daher können auch asynchrone Rückrufe zu Deadlocks führen. Dieses Fehlerniveau ist auf die unsachgemäße Verwendung des synchronisierten Schlüsselworts zurückzuführen.
Dann ist es ein unsichtbares Mutex-Objekt Was genau ist das?
Es ist einfach, an die Instanz selbst zu denken, denn auf diese Weise müssen Sie kein neues Objekt definieren und sperren. Um diese Idee zu beweisen, können Sie ein Programm schreiben, um sie zu beweisen.
Die Idee ist sehr einfach: Definieren Sie eine Klasse mit zwei Methoden. Eine Methode wird als synchronisiert deklariert, eine verwendet synchronisiert (dies) im Methodenkörper und startet dann zwei Threads, um diese beiden Methoden jeweils aufzurufen Tritt zwischen den beiden Methoden auf (wait), kann erklärt werden, dass der unsichtbare Mutex in der synchronisierten Methodendeklaration tatsächlich die Instanz selbst ist

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();
 
  }
 
}
Die Ergebnisausgabe lautet:

m1 call
m1 call done
m2 call
m2 call done
Erklärung, dass der Sync-Block der Methode m2 wartet. Auf diese Weise kann die obige Annahme bestätigt werden.

Es sollte auch beachtet werden, dass beim Hinzufügen von sync zu einer statischen Methode die Sperre auftritt, da es sich um eine Methode auf Klassenebene handelt Das Objekt gehört zur aktuellen Klasse. Sie können es auch beweisen. Dies wird hier weggelassen. Daher kann das synchronisierte Schlüsselwort der Methode beim Lesen automatisch ersetzt werden. Das ist leicht zu verstehen.

Beginnen wir mit der Speichersichtbarkeit von Synchronized
                    void method(){
void synchronized method(){         synchronized(this){
   // biz code                // biz code
}               ------>>>   }
                    }
In Java wissen wir alle, dass das Schlüsselwort synchronisiert verwendet werden kann, um gegenseitigen Ausschluss zwischen Threads zu implementieren, aber wir vergessen es oft dass es eine andere Funktion hat, nämlich die Sichtbarkeit von Variablen im Speicher sicherzustellen. Das heißt, wenn zwei Threads, lesend und schreibend, gleichzeitig auf dieselbe Variable zugreifen, wird synchronisiert verwendet, um sicherzustellen, dass der schreibende Thread die Variable aktualisiert, Der Lesethread kann den neuesten Wert der Variablen lesen, wenn er erneut auf die Variable zugreift.


Zum Beispiel das folgende Beispiel:

Was wird Ihrer Meinung nach der Lesethread ausgeben? 42? Unter normalen Umständen wird 42 ausgegeben. Aufgrund von Neuordnungsproblemen gibt der Lesethread jedoch möglicherweise 0 oder nichts aus.
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;
  }
}

Wir wissen, dass der Compiler den Code möglicherweise neu anordnet, wenn er Java-Code in Bytecode kompiliert, und dass die CPU möglicherweise auch ihre Anweisungen neu anordnet, wenn sie Maschinenanweisungen ausführt. Die Sortierung beeinträchtigt nicht die Semantik des Programms -

在单一线程中,只要重排序不会影响到程序的执行结果,那么就不能保证其中的操作一定按照程序写定的顺序执行,即使重排序可能会对其它线程产生明显的影响。
这也就是说,语句"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;
    }
  }
}

这个是因为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
}

我们假设线程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中文网!


Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn