オーディオとビデオの開発ツアー (53) - Java 並行プログラミング同期

 

目次

  1. 同期の使い方
  2. シンクロの原理
  3. スレッド待機、割り込み、ウェイクアップ
  4. 材料
  5. 褒美

1.シンクロナイズドの使い方

キーワード synchronized を使用すると、1 つのスレッドだけが特定のメソッドまたは特定のコード ブロックを同時に実行できるようになります。

  • インスタンス メソッドを変更し、現在のインスタンスに作用してロックし、同期コードを入力する前に現在のインスタンスのロックを取得します。

     synchronized void syncIncrease4Obj(){
        synchronized (this){
            i++;
        }
    }
  • 現在のクラス オブジェクトのロックに作用し、同期コードを入力する前に現在のクラス オブジェクトのロックを取得する変更された静的メソッド

    synchronized static void syncIncrease(){
        synchronized (this){
            i++;
        }
    }
  • 同期コード ライブラリに入る前に、コード ブロックを変更し、ロック オブジェクトを指定し、特定のオブジェクトをロックし、特定のオブジェクトのロックを取得します。
    メソッド本体は比較的大きく、同時に時間のかかる操作がいくつかあり、同期する必要があるのはコードのごく一部のみです. メソッド全体が直接同期される場合、利益は損失に値しない可能性があります. . この時点で、コード ブロックを同期する方法を使用できます。同期が必要なコードをラップします。

   public void syncIncrease(){
        synchronized (this){
            i++;
        }
    }

第二に、同期の原則

2.1 逆アセンブル コードを表示する

生成されたクラス ファイルを javap で確認すると、同期コード ブロックの実装で monitorenter および monitorexit 命令が使用されていることがわかります。同期メソッドは、修飾子 ACC_SYNCHRONIZED に依存して完了します。

同期修正メソッドの逆アセンブルをまず見てください


    public static synchronized void increase(){
        i++;
    }

    public synchronized void increase4Obj(){
        i++;
    }

---> javap -c -v Main.class は、 対応するアセンブリを次のように逆コンパイルします。

public static synchronized void syncIncrease();
   descriptor: ()V
   flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED  //同步标示
   Code:
     stack=2, locals=0, args_size=0
        0: getstatic     #2                  // Field i:I
        3: iconst_1
        4: iadd
        5: putstatic     #2                  // Field i:I
        8: return
     LineNumberTable:
       line 12: 0
       line 13: 8

 public synchronized void syncIncrease4Obj();
   descriptor: ()V
   flags: ACC_PUBLIC, ACC_SYNCHRONIZED.        //同步标示
   Code:
     stack=2, locals=1, args_size=1
        0: getstatic     #2                  // Field i:I
        3: iconst_1
        4: iadd
        5: putstatic     #2                  // Field i:I
        8: return
     LineNumberTable:
       line 19: 0
       line 20: 8

ACC_SYNCHRONIZED フラグは、メソッドが同期メソッドであることを示し、JVM は ACC_SYNCHRONIZED アクセス フラグを使用して、メソッドが同期メソッドとして宣言されているかどうかを識別し、それによって対応する同期呼び出しを実行します。

コードブロックで同期を使用した逆コンパイルを見てみましょう

 
   public void syncIncrease(){
        synchronized (this){
            i++;
        }
    }

-->javap -c Main.class 逆コンパイル対応アセンブリは以下の通りです。

public void syncIncrease();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter                //进入同步方法
       4: aload_0
       5: dup
       6: getfield      #2                  // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2                  // Field i:I
      14: aload_1
      15: monitorexit                 //退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit                 //退出同步方法(这个是针对异常处理的)
      22: aload_2
      23: athrow
      24: return

Q: 上記のバイトコードからわかるように、追加の monitorexit 命令があります。なぜですか?
A: メソッドが異常終了したときに、monitorenter 命令と monitorexit 命令を正しくペアにして実行できるようにするために、コンパイラは自動的に例外ハンドラを生成し、メソッドが異常終了したときにモニタを解放する命令が実行されます。 .

いずれの方法を使用する場合でも、本質はオブジェクトのモニターを取得することであり、この取得は排他的です。つまり、同期によって保護されたモニターを同時に取得できるスレッドは 1 つだけです。モニターを取得しないスレッドは、同期ブロックまたは同期メソッドの入り口でブロックされ、BLOCKED 状態になります。

画像: 「Java 並行プログラミングの技術」

monitorenter 命令が実行されると、現在のスレッドは objectref に対応するモニターの所有権 (つまり、オブジェクト ロック) を取得しようとします. objectref のモニターのエントリ カウンターが 0 の場合、スレッドは正常に取得できます.監視し、カウンター値を 1 に設定すると、ロックが正常に取得されます。
現在のスレッドが既に objectref のモニターを所有している場合は、モニターに再度入ることができ、再入時にカウンターの値が 1 増加します。
他のスレッドが既に objectref のモニターを所有している場合、現在のスレッドは、実行中のスレッドの実行が終了するまでブロックされます。つまり、monitorexit 命令が実行されます。

では、スレッドがオブジェクトのモニターを取得したかどうかをどのように知るのでしょうか?
その答えを見つけるために、一緒に JVM 内のオブジェクトのメモリ レイアウトを学びましょう。

2.2 オブジェクトヘッダー

JVM では、メモリ内のオブジェクトのレイアウトは、オブジェクト ヘッダー、インスタンス データ、アラインメント パディングの 3 つの領域に分割されます。

画像ソース: Java 同時実行の同期実装の原則に関する深い理解

オブジェクト ヘッダー: 同期ロックを実装するための基礎です。
インスタンス変数: クラスの属性データ情報を格納します, 親クラスの属性情報を含みます. 配列のインスタンス部分である場合, 配列の長さも含まれます. この部分は4-に従って
データで埋められます.バイト アラインメント: バイト アラインメントに使用されます仮想マシンでは、オブジェクトの開始アドレスが 8 バイトの整数倍である必要があります。

同期ロックオブジェクトの基本となる Java ヘッダーオブジェクトと、同期によって使用されるロックオブジェクトは、Java オブジェクトヘッダーの Mark Word に格納されます。

画像: 「Java 並行プログラミングの技術」

2.3 重量ロック、偏ったロック、軽量ロック

2.3.1 重量ロック

Java 1.6 より前では、同期には重いロックしかなく、Mark Word ポインターはモニター オブジェクトを指していましたが、Java 仮想マシン (HotSpot) では、モニターは ObjectMonitor によって実装され、その主なデータ構造は次のようになります。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

モニター ロック (モニター) は、基盤となるオペレーティング システムのミューテックス ロックに依存することによって実装されます. オペレーティング システムがスレッド間で切り替わるとき、ユーザー状態からコア状態に切り替える必要があります. これらの状態間の遷移には比較的長い時間がかかります.時間 時間、時間コストは比較的高いです。

Java 6 以降、同期は JVM レベルから大幅に最適化され、軽量ロックとバイアス付きロックが導入され、ロックの取得と解放によるパフォーマンスの消費が削減されました。

2.3.2 バイアスロック

バイアス ロックの核となる考え方は、スレッドがロックを取得すると、ロックがバイアス モードになるということです. このとき、Mark Word の構造もバイアス ロック構造になります. スレッドが再度ロックを要求すると、同期操作、つまり、ロックを取得するプロセスを実行する必要はありません。これにより、ロック アプリケーションに関連する多くの操作が節約され、プログラムのパフォーマンスが向上します。

2.3.3 軽量ロック

プログラムのパフォーマンスを向上させるための軽量ロックの根拠は、「ほとんどのロックでは、同期サイクル全体で競合が発生しない」ということです。これは経験的なデータであることに注意してください。軽量ロックが適している場面は、スレッドが交互に同期ブロックを実行する場合であり、同じロックに同時にアクセスする場合があると、軽量ロックが重量ロックに発展することに注意してください。

スピンロック

軽量ロックが失敗した後、仮想マシンはスピン ロックと呼ばれる最適化方法を実行して、オペレーティング システム レベルでスレッドが実際にハングするのを防ぎます。これは、ほとんどの場合、スレッドが長時間ロックを保持しないという事実に基づいています. オペレーティング システム レベルでスレッドを直接中断すると、損失に見合わない可能性があります. 結局、オペレーティング システムは、スレッド間を切り替えるとき、ユーザー状態からスレッドに切り替えます. コア状態では、これらの状態間の遷移に比較的長い時間がかかり、時間コストが比較的高いため、スピンロックは現在のスレッドがロックを取得できると想定しています近い将来、仮想マシンは現在ロックを取得しようとしているスレッドにいくつかの空のループを実行させます (これがスピンと呼ばれる理由です)。ループし、ロックを取得するとクリティカルセクションにスムーズに入ることができます。ロックを取得できない場合、OS レベルでスレッドをサスペンドする、これがスピンロックの最適化手法であり、この手法により確かに効率が向上します。結局、重いロックにアップグレードするしか方法はありません
引用元:強くお勧めします - Java 同時実行の同期実装の原則を深く理解することをお勧めします

ロック解除

JIT コンパイル中に、Java 仮想マシンは実行中のコンテキストをスキャンして、共有リソースの競合が発生する可能性が低いロックを削除します. このように不要なロックを削除することで、ロックを要求するための無意味な時間を節約できます.

3. スレッド待ち、割り込み、起床

3.1 待機/通知メカニズム

これらのメソッドは Object クラスで定義されているため、待機/通知の関連メソッドはどの Java オブジェクトでも使用できます。

  • notify: オブジェクトを待機しているスレッドに wait メソッドから戻ることを通知し、スレッドがオブジェクトのロックを取得することを前提に 戻ります. notify は、待機キューで待機中のスレッドを同期キューに移動し、状態を移動したスレッドの WAITING が BLOCKED になります

  • notifyAll: このオブジェクトを待機しているすべてのスレッドに通知します。

  • wait: このメソッドを呼び出すスレッドは WAITING 状態になり、現在のスレッドをオブジェクトの待機キューに入れます。別のスレッドが通知または中断されるのを待ってからのみ返されます. wait メソッドを呼び出した後、オブジェクトのロックが解放されることに注意してください.

  • wait(long): タイムアウトで一定時間待機. ここでのパラメータ time はミリ秒です, つまり, 最大 n ミリ秒待機します. 通知がない場合, タイムアウト後に戻ります.

  • Wait(long ,int) タイムアウト時間をより細かく制御するために、ナノ秒に達することがあります。

上記のメソッドを使用する場合は、同期されたコード ブロックまたは同期されたメソッドにいる必要があります。そうしないと、IllegalMonitorStateException がスローされます

3.2 割り込みとウェイクアップ

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();
  • スレッドがブロックされた状態またはブロック操作を実行しようとしている場合は、Thread.interrupt() メソッドを使用してスレッドを中断します。この時点で InterruptedException がスローされ、中断された状態がリセットされることに注意してください (から変更されます)。中断された状態から中断されていない状態へ)

  • スレッドが実行中の状態の場合、インスタンス メソッドの interrupt() を呼び出してスレッドを中断することもできますが、同時に、手動で中断ステータスを判断する必要があり、スレッドを中断するコード (実際には、 run メソッドの本体を終了するコード) を記述する必要があります。

public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判断当前线程是否被中断
                    if (this.isInterrupted()){
                        System.out.println("线程中断");
                        break;
                    }
                }

                System.out.println("已跳出循环,线程中断!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

    }

上記の 2 点を総合すると、次のように判断できます。

public void run(){
    try {
    //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
    while (!Thread.interrupted()) {
        TimeUnit.SECONDS.sleep(2);
    }
    } catch (InterruptedException e) {

    }
}
  • 取得待ちのロックオブジェクトの同期メソッドやコードブロックに対してスレッドの割り込み操作が効かず、オブジェクトのロックが他のスレッドに占有されているため、待ちスレッドはロック待ちしかできません。 、 thread.interrupt(); を呼び出しますが、スレッドを中断することはできません。

材料

  1. 本:「Java並行プログラミングの芸術」
  2. 強くお勧めします - Java 同時実行の同期実装の原則を深く理解していること

褒美

この記事の学習実践を通して

  1. 同期の基本的な使用方法を確認しました
  2. 同期同期の原理を学んだ
  3. 同期のための JVM の最適化を理解する
  4. スレッドの待機、割り込み、およびウェイクアップの学習レビュー

お読みいただきありがとうございます.
次の記事では、Java 並行プログラミング シリーズ - ロック関連の学習と実践を続けます. ようこそ、公式アカウント「オーディオとビデオの開発の旅」に注目して、一緒に学び、成長してください.
交換へようこそ

おすすめ

転載: blog.csdn.net/u011570979/article/details/119767727