ホームページ  >  記事  >  Java  >  Java の synchronized キーワードのデッドロックとメモリ使用量の問題の詳細な説明

Java の synchronized キーワードのデッドロックとメモリ使用量の問題の詳細な説明

高洛峰
高洛峰オリジナル
2017-01-05 16:10:341958ブラウズ

まず synchronized の詳細な説明を見てみましょう:
synchronized は Java 言語のキーワードであり、メソッドまたはコード ブロックを変更するために使用すると、最大 1 つのスレッドがコードを同時に実行することが保証されます。

1. 2 つの同時スレッドが同じオブジェクト内の同期された (この) 同期コード ブロックにアクセスする場合、一度に 1 つのスレッドのみを実行できます。別のスレッドは、このコード ブロックを実行する前に、現在のスレッドがこのコード ブロックの実行を完了するまで待つ必要があります。

2. ただし、スレッドがオブジェクトの同期された (この) 同期されたコード ブロックにアクセスするとき、別のスレッドはオブジェクト内の非同期な (この) 同期されたコード ブロックに引き続きアクセスできます。

3. 特に重要なのは、スレッドがオブジェクトの同期された (この) 同期されたコード ブロックにアクセスすると、他のスレッドはオブジェクト内の他のすべての同期された (この) 同期されたコード ブロックにアクセスできなくなることです。

4. 3 番目の例は、他の同期コード ブロックにも適用できます。つまり、スレッドがオブジェクトの synchronized (this) 同期コード ブロックにアクセスすると、このオブジェクトのオブジェクト ロックを取得します。その結果、オブジェクト オブジェクトのすべての同期されたコード部分への他のスレッドのアクセスが一時的にブロックされます。

5. 上記のルールは他のオブジェクト ロックにも適用されます。
簡単に言うと、同期とは、このロックを所有するスレッドがブロック内の命令を実行できるようにすることです。ロックを取得すると、同じ操作を実行できます。
これは非常に便利ですが、同じクラス内で 2 つのメソッドが synchronized で宣言されています
2。メソッドを実行する際には、別のメソッド(非同期スレッドのコールバック)が実行されるのを待つ必要があるため、countDownLatch を使用して待機します。
3. コードは次のように分解されます。

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

このうち、メソッド a が実行されます。スレッド実行後のコールバックはメインスレッドによって実行され、メソッド b は非同期メソッドによって実行されます。実行結果は次のとおりです。

メソッド a の実行後にメインスレッドがスタックし始め、どれだけ待っても続行されなくなります。 , それは役に立ちません。
これは非常に古典的なデッドロックの問題です
a は b の実行を待っています。実際、b をコールバックとして見ないでください。b も同期された動作を待っています。なぜでしょうか。
一般的に言えば、コードのブロックを同期したいときは、次のように共有変数を使用してロックする必要があります:

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

メソッド a とメソッド b の内容をメソッド a1 とメソッド a1 の同期されたブロックに移行すると、それぞれ b1 とするとわかりやすいでしょう。
a1 が実行された後、間接的に (countDownLatch) メソッド b1 が実行されるのを待ちます。

ただし、ミューテックスが解放されていないため、b1 を待ち始めます。このとき、非同期コールバックメソッド b1 が必要であっても、ミューテックスがロックを解除するまで待つ必要があるため、b メソッドは実行されません。

これによりデッドロックが発生します。
ここでの synchronized キーワードは、メソッドの前に配置しても同じ効果があります。これは、Java 言語が同じオブジェクト内の synchronized メソッドで使用されるミューテックスを隠すのに役立つだけです。非同期コールバックはデッドロックを引き起こす可能性があるため、このレベルのエラーは synchronized キーワードの不適切な使用が原因であることに注意してください。無差別に使用せず、正しく使用してください。では、そのような非表示のミューテックス オブジェクトとは一体何なのでしょうか。 ?
このアイデアを証明するために、新しいオブジェクトを定義してロックを行う必要がないため、インスタンス自体を考えるのは簡単です。
このアイデアは非常に優れています。単純に、クラスを定義して 2 つのメソッドを持ちます。1 つのメソッドは synchronized として宣言され、もう 1 つはメソッド本体で synchronized(this) を使用し、2 つのスレッド間でロックの競合 (待機) が発生した場合に、これら 2 つのメソッドをそれぞれ呼び出すために 2 つのスレッドを開始します。メソッドの宣言は説明できます。 synchronized の目に見えないミューテックスは実際にはインスタンスそのものです。

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();
 
  }
 
}

結果の出力は次のとおりです。
m1 call
m1 call done
m2 call
m2 call done

これは、メソッド m2 の同期ブロックが m1 の実行を待っていることを意味します。

さらに、静的メソッドに sync を追加する場合、それはクラスレベルのメソッドであるため、ロックされたオブジェクトは現在のクラスのクラス インスタンスであることを説明する必要があります。それを証明するプログラムです。ここでは省略します。

それで、メソッドの synchronized キーワードは、読み込むときに自動的に synchronized(this){} に置き換えることができます。

                    void method(){
void synchronized method(){         synchronized(this){
   // biz code                // biz code
}               ------>>>   }
                    }

のメモリの可視性から始めましょう。 Synchronized
Java では、キーワード synchronized を使用してスレッド間相互排他を実装できることは誰もが知っていますが、これには別の役割があることを忘れがちですが、それはメモリ内の変数の可視性を確保することです。つまり、2 つのスレッドが実行されているときです。読み取りと書き込みでは、同時に同じ変数にアクセスします。同期は、書き込みスレッドが変数を更新した後に確実に変数を更新するために使用されます。読み取りスレッドが変数に再度アクセスすると、変数の最新の値を読み取ることができます。

たとえば、次の例を考えてみましょう:

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;
  }
}

読み取りスレッドは何を出力すると思いますか? 42? 通常の状況では 42 が出力されますが、並べ替えの問題により、読み取りスレッドが 0 または何も出力しない場合があります。

Java コードをバイトコードにコンパイルするときにコンパイラーがコードの順序を変更する可能性があること、また、プログラムのセマンティクスが破壊されない限り、CPU が機械語命令を実行するときにその命令の順序を変更する場合があることはわかっています。

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


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。