Javaコンカレントプログラミング:Synchronizedとその実現原理

1. Synchronizedの基本的な使用法

同期化は、Javaの同時実行性の問題を解決するために最も一般的に使用される方法の1つであり、最も簡単な方法でもあります。Synchronizedには3つの主要な機能があります:(1)スレッドが相互に排他的に同期コードにアクセスできるようにする(2)シェア変数の変更を時間内に確認できるようにする(3)並べ替えの問題を効果的に解決する 構文的には、Synchronizedには3つの使用法があります。


1.通常のメソッドの
変更2.静的メソッドの
変更3.コードブロックの変更

次に、これらの3つの使用方法を示すためにいくつかのサンプルプログラムを使用します(比較のために、3つのコードは、Synchronizedの使用を除いて基本的に同じです)。

1.同期がない場合:

コードスニペット1:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

実行結果は、スレッド1とスレッド2が同時に実行状態になり、スレッド2の方がスレッド1よりも速く実行されるため、スレッド2が最初に実行され、このプロセスではスレッド1とスレッド2が同時に実行されます。

Method 1 start
Method 1 execute
Method 2 start
Method 2 execute
Method 2 end
Method 1 end

2.一般的な方法と同期します。

コードスニペット2:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public synchronized void method1(){
        System.out.println("Method 1 start");
        try {
            System.out.println("Method 1 execute");
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public synchronized void method2(){
        System.out.println("Method 2 start");
        try {
            System.out.println("Method 2 execute");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

実行結果は以下のようになりますが、スレッド2はコードセグメントと比較して、スレッド1のメソッド1の実行が完了するのを待ってから、メソッド2メソッドの実行を開始する必要があります。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

3.静的メソッド(クラス)の同期

コードスニペット3:

package com.paddx.test.concurrent;
 
 public class SynchronizedTest {
     public static synchronized void method1(){
         System.out.println("Method 1 start");
         try {
             System.out.println("Method 1 execute");
             Thread.sleep(3000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 1 end");
     }
 
     public static synchronized void method2(){
         System.out.println("Method 2 start");
         try {
             System.out.println("Method 2 execute");
             Thread.sleep(1000);
         } catch (InterruptedException e) {
             e.printStackTrace();
         }
         System.out.println("Method 2 end");
     }
 
     public static void main(String[] args) {
         final SynchronizedTest test = new SynchronizedTest();
         final SynchronizedTest test2 = new SynchronizedTest();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test.method1();
             }
         }).start();
 
         new Thread(new Runnable() {
             @Override
             public void run() {
                 test2.method2();
             }
         }).start();
     }
 }

実行結果は次のとおりです。静的メソッドの同期は基本的にクラスの同期です(静的メソッドは基本的にオブジェクトのメソッドではなく、クラスに属するメソッドです)、testとtest2が異なるオブジェクトに属していても、どちらもSynchronizedTestに属しますクラスのインスタンス。method1とmethod2は同時に実行することはできず、シーケンシャルにのみ実行できます。

Method 1 start
Method 1 execute
Method 1 end
Method 2 start
Method 2 execute
Method 2 end

4.コードブロックの同期

コードスニペット4:

package com.paddx.test.concurrent;

public class SynchronizedTest {
    public void method1(){
        System.out.println("Method 1 start");
        try {
            synchronized (this) {
                System.out.println("Method 1 execute");
                Thread.sleep(3000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 1 end");
    }

    public void method2(){
        System.out.println("Method 2 start");
        try {
            synchronized (this) {
                System.out.println("Method 2 execute");
                Thread.sleep(1000);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Method 2 end");
    }

    public static void main(String[] args) {
        final SynchronizedTest test = new SynchronizedTest();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method1();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                test.method2();
            }
        }).start();
    }
}

実行結果は次のとおりです。スレッド1とスレッド2の両方が対応するメソッドに入って実行を開始しますが、スレッド2はスレッド1の同期ブロックの実行が完了するのを待ってから同期ブロックに入る必要があります。

Method 1 start
Method 1 execute
Method 2 start
Method 1 end
Method 2 execute
Method 2 end

2.同期原理

上記の実行結果について質問がある場合は、心配しないでください。最初に同期の原理を理解しましょう。そうすれば、上記の問題が一目でわかります。最初に、Synchronizedが次のコードを逆コンパイルしてコードブロックを同期する方法を見てみましょう。

package com.paddx.test.concurrent;

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

逆コンパイル結果:

これら2つの命令の役割については、JVM仕様の説明を直接参照します。

monitorenter:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:

• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.

• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.

• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

この一節の一般的な意味は次のとおりです。

各オブジェクトには、モニターロック(モニター)があります。モニターが占有されると、モニターはロックされ、スレッドがmonitorenter命令を実行すると、モニターの所有権を取得しようとします。プロセスは次のとおりです。

モニターのエントリー数が0の場合、スレッドはモニターに入り、エントリー数は1に設定され、スレッドはモニターの所有者です。

スレッドがすでにモニターを占有していて、再び入っただけの場合、モニターに入るエントリーの数は1ずつ増加します。

他のスレッドがすでにモニターを占有している場合、モニター内のエントリー数が0になるまでスレッドはブロッキング状態に入り、モニターの所有権の取得を再試行します。

monitorexit: 

monitorexitを実行するスレッドは、objectrefによって参照されるインスタンスに関連付けられているモニターの所有者である必要があります。

スレッドは、objectrefに関連付けられているモニターのエントリ数を減らします。その結果、エントリカウントの値がゼロの場合、スレッドはモニターを終了し、その所有者ではなくなります。モニターに入ることをブロックしている他のスレッドは、そうすることを試みることができます。

この一節の一般的な意味は次のとおりです。

monitorexitを実行するスレッドは、objectrefに対応するモニターの所有者でなければなりません。

命令が実行されると、モニターのエントリー番号が1つ減ります。1を引いた後、エントリー番号が0の場合、スレッドはモニターを終了し、モニターの所有者ではなくなります。このモニターによってブロックされた他のスレッドは、このモニターの所有権を取得しようとする可能性があります。 

説明のこれら2つの段落を通して、Synchronizedの実装原理を明確に見ることができるはずです。Synchronizedのセマンティックボトムは、モニターオブジェクトによって完了します。実際には、待機/通知やその他のメソッドもモニターオブジェクトに依存しています。待機/通知およびその他のメソッドをブロックまたはメソッドで呼び出すことができます。それ以外の場合は、java.lang.IllegalMonitorStateExceptionがスローされます。

同期メソッドの逆コンパイル結果を見てみましょう。

ソースコード:

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

逆コンパイル結果:

逆コンパイルの結果から、メソッドの同期は、monitorenterとmonitorexitの命令では完了しません(理論的には、これらの2つの命令でも実行できます)。ただし、通常の方法と比較すると、定数プールにはACC_SYNCHRONIZED識別子が追加されています。 。JVMは、この識別子に基づいてメソッドの同期を実装します。メソッドが呼び出されると、呼び出し元の命令は、メソッドのACC_SYNCHRONIZEDアクセスフラグが設定されているかどうかを確認します。設定されている場合、実行スレッドが最初にモニターを取得し、取得が成功した後にメソッド本体を実行できます。 、そしてメソッドが実行された後にモニターをリリースします。メソッドの実行中、他のスレッドは同じモニターオブジェクトを取得できません。実際、本質的に違いはありませんが、メソッドの同期はバイトコードなしで暗黙的に実現されます。

三、演算結果解釈

Synchronizedの原理を理解すれば、上記のプログラムをもう一度見ることで解決できます。

1.コードセグメント2の結果:

method1とmethod2は異なるメソッドですが、これら2つのメソッドは同期され、同じオブジェクトを介して呼び出されるため、呼び出す前に、同じオブジェクトのロック(モニター)について競合する必要があります。ロックは相互に排他的に取得できるため、method1とmethod2は順番にしか実行できません。

2.コードセグメント3の結果:

testとtest2は異なるオブジェクトに属していますが、testとtest2は同じクラスの異なるインスタンスに属しています。method1とmethod2は静的同期メソッドであるため、呼び出し時に同じクラスのモニターを取得する必要があります(各クラスは1つのクラスオブジェクトにのみ対応します) )、そのため、順次実行のみ可能です。

3.コードセグメント4の結果:

コードブロックの同期では、基本的に、Synchronizedキーワードの後のかっこ内のオブジェクトのモニターを取得する必要があります。このコードのかっこの内容はこれであり、method1とmethod2は同じオブジェクトを通じて呼び出されるため、同期ブロックに入る前に同じオブジェクトのロックをめぐって競合する必要があるため、同期したブロックのみを順次実行できます。

4、まとめ

同期は、Java並行プログラミングでスレッドの安全性を確保するために最も一般的に使用される方法であり、その使用は比較的簡単です。しかし、その原理を深く理解し、モニターロックの基本的な知識を理解できれば、一方ではSynchronizedキーワードを正しく使用でき、他方で並行プログラミングメカニズムをよりよく理解して、さまざまな状況でタスクを完了するには、より最適な同時方式を選択してください。また、平時に遭遇するさまざまな並行性の問題に落ち着いて対処することもできます。

おすすめ

転載: blog.csdn.net/bj_chengrong/article/details/96830136