エントリから高度な(11)へのマルチスレッド---同期ロックの実装原理

1つは、同期の特徴です。

同時実行性の高いプログラミングでは、スレッドセーフは焦点を当てる必要のあるトピックであり、スレッドセーフを引き起こす2つの側面があります。

  • 共有リソースまたは重要なリソースが必要
  • 複数のスレッドの同時操作

上記の2つの条件が満たされると、スレッドセーフの問題が発生する可能性があります。解決策は非常に簡単です。つまり、この共有変数を常に操作するスレッドを制御することです。これはミューテックスロックです。Synchronizedは、一種の相互排他ロックです。Synchronizedは、ロックされた重要なリソースに同時に1つのスレッドのみが入るようにすることができます。Synchronizedには、次の特性もあります。

  • Atomicity:これは、スレッドセーフを確保するためのSynchronizedの重要な前提条件です。アトミック操作は一緒に成功するか、完全に失敗します。つまり、中間操作は分割できません。たとえば、i ++のコードは、実際には、基になるバイトコードが次のように分割されます。 3つの読み取り、カウント、および割り当ては2つのステップで実行されます。同期の変更が追加された場合、これらの3つの操作は1つであり、分割できません。volatile和sync的重要区别之一就是sync具有原子性和volatile不具有原子性

  • 可視性:可視性とは、同期によってロックされたコード内の共有変数に対する操作が他のスレッドから見えることを意味します。これには、Javaのスレッドモデルも含まれます。JMM(Javaスレッドモデル)では、すべての共有変数が存在する必要があると規定されています。メインメモリ内、各スレッドに作業領域がある場合、スレッドによる共有変数の操作は作業領域で実行する必要があります。つまり、メインメモリ内の共有変数を独自の作業領域にコピーしてから再度変更する必要があります。作業領域に戻すと、各スレッド間のデータ交換はメインメモリを介して完了する必要があり、異なる作業領域間でデータを転送することはできません。

  • 順序:プログラムの効率を向上させるために、Javaは命令プログラムを並べ替えますが、同時実行性が高い場合、この並べ替えによってプログラムの実行結果が変わる可能性があるため、同期によって順序が保証されます。つまり、JVMでは順序が保証されません。命令の並べ替えを実行します

  • 再入可能性:現在のロックを所有するスレッドは、現在のロックを繰り返し適用することもできます。synchronized和ReentrantLock都是可重入锁

Synchronizedは常にヘビーウェイトロックと呼ばれていましたが、jdk1.6以降、ロックとリリースロックのパフォーマンス消費を削減するために、バイアスロックとライトウェイトロックが導入されました。

そして、ロックのストレージ構造とアップグレードプロセス

第二に、同期ロック同期の原理

1.同期コードブロックをロックします

public class SynchronizedDemo {
    
    
    Object lock = new Object();
    public void test1(){
    
    
        synchronized (lock){
    
    };
    }
}

JVMの最下層がどのように実装されているかを確認するには、javap命令を使用してバイトコードファイルを表示するのが最も直接的な方法です。

  public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: getfield      #3                  // Field lock:Ljava/lang/Object;
         4: dup
         5: astore_1	
         6: monitorenter		// !!! 关注这里
         7: aload_1
         8: monitorexit			// !!! 关注这里
         9: goto          17
        12: astore_2
        13: aload_1
        14: monitorexit
        15: aload_2
        16: athrow
        17: return
      Exception table:
         from    to  target type
             7     9    12   any
            12    15    12   any
      LineNumberTable:
        line 6: 0
        line 7: 17
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 12
          locals = [ class cn/java/SynchronizedDemo, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

逆コンパイルの結果から、同期方式の前後にmonitorenterとmonitorexitがありますが、この2つの単語を見ると、モニターに出入りする操作が2つあることが大まかにわかります。これについては後で説明します。これでロックとして理解できるようになり、このロックは新しいオブジェクトになるとすぐに各オブジェクトによって保持されます。

monitorenter的过程:

  • オブジェクトのモニターエントリ番号が0の場合、スレッドはモニターに入り、モニターエントリ番号が1増加すると、スレッドはモニターのユーザーになります。
  • スレッドがすでにモニターを保持していて、それを再取得したい場合は、モニターのエントリ番号がもう一度1つ増えます。これは、再入可能性の実現原則です。
  • スレッドがモニターのエントリ数が0でないことを検出した場合、つまりモニターが他のスレッドによって占有されている場合、スレッドはモニターのエントリ数が0になるまでブロックし、の所有権を取得します。もう一度監視する

monitorexit的过程:

  • モニター出口を実行するスレッドは、モニターのホルダーである必要があります。命令が実行されると、モニターのエントリー番号は1つ減ります。モニターの数が0の場合、スレッドはモニターを終了し、それ以上はなくなります。モニターを使用する権利を保持します。このとき、他のスレッドはモニターを使用する権利を取得できます。

2.同期方法をロックします

public class SynchronizedDemo {
    
    
    synchronized public void test2(){
    
    }
}

同じようにjavapを使用してコードを逆コンパイルします


  public synchronized void test2();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 11: 0

上記の逆コンパイル結果の分析から、monitorenterとmonitorexitは使用されませんが、公式のJava仮想マシン仕様ドキュメントに直接記載されている追加のタグACC_SYNCHRONIZEDが定数プールにあります。

synchronized方法は、通常使用して実装されていないmonitorentermonitorexitをむしろ、実行時定数プールACC_SYNCHRONIZEDでは、メソッド呼び出し命令(§2.11.10によってチェックされるフラグによって単純に区別されます

同期方法は通常、monitorenterとmonitorexitを使用して実装されません。それどころか、それは、メソッド呼び出し命令によってチェックされる実行時定数プールのACC_SYNCHRONIZEDフラグによってのみ区別されます。

あなたはチャプター2.11.10を引き続き見ることができます、公式文書はそれを詳細に説明しています。メソッドレベルの同期は暗黙的に実行されます。ACC_SYNCHRONIZEDフラグを設定するメソッドが呼び出されると、実行スレッドがモニターを取得します。取得が成功すると、メソッド本体を実行でき、メソッドの実行後にモニターが解放されます。

言い換えれば、本質的に、Synchronizedによってロックされた同期メソッドと同期コードブロックはモニターによって実装されますが、同期メソッドは暗黙の実行にすぎません。

3.javaオブジェクトヘッダー

特別な指示がない場合、以下はすべてHostopの仮想マシンです。仮想マシンメモリ内のオブジェクトのレイアウトは、次の3つの部分に分けることができます。

  • オブジェクトヘッダー
  • インスタンスデータ
  • データ入力

Synchronizedに関連するのは、オブジェクトヘッダーのデータです。パディングデータは必須の部分ではありませんが、オブジェクトの開始アドレスは8バイトの整数倍である必要があります。パディングデータはバイトアラインメント専用です。

メインオブジェクトヘッダ構造がで構成されMark Word(标记字段)Class Metadata Address(类型指针)メタデータ・ポインタに格納されているアドレスクラス・オブジェクト・タイプ、仮想マシンは、オブジェクトがこのポインタのインスタンスであることによって決定され、オブジェクトは、アレイは、追加の記憶装置である場合にされていますArray length配列の長さの値になります

長さ コンテンツ 説明
32/64ビット マークワード オブジェクトのハッシュコードまたはロック情報を保存します
32/64ビット クラスメタデータアドレス オブジェクトタイプデータを格納するためのポインタ
32/32ビット 配列の長さ 配列の長さ

64ビット仮想マシンでは、Mark Wordは64ビットであり、そのストレージ構造は次のとおりです。

img

さまざまなロックフラグに応じて、MarkWordに保存されているデータがそれに応じて変更されます。次に例を示します。

img

その中でも、ロックの最適化については、バイアスロック、ライトウェイトロック、ヘビーウェイトロックが関係しますが、現時点では、マークワードのオブジェクトヘッダーに格納できるデータのみを紹介します。

4.モニター

Synchronizedがコードブロックまたはメソッドをロックする場合、モニターが関与します。Javaによると、すべてがオブジェクトです。モニターはオブジェクトまたはツールとして理解できます。このツールは、Synchronizedロックの同期メカニズムを保証できます。すべてのJavaオブジェクトには独自のモニターオブジェクトがあります

モニターのソースコードは、仮想マシンのソースコードにあります。

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

主に上記の2つのコンテナに注意を払うために、waitSetとEntryListは、何らかの理由で現在待機しているスレッドを保存するために使用されます。_ownerは、現在のモニターを保持しているスレッドを指します。複数のスレッドが同時にロックを取得する場合、プロセスは次のとおりです。

  1. スレッドは_EntryListコレクションに入り、待機します。スレッドがオブジェクトのモニターを取得すると、モニターの所有者変数を現在のスレッドに設定し、カウント変数を設定します++
  2. スレッドがwait()メソッドを呼び出すと、現在のモニターの保持権が解放され、所有者がnullに復元され、カウントが1つ減り、スレッドはwaitSetコレクションに入り、ウェイクアップされるのを待ちます。
  3. 現在の使用権を持つスレッドがコードの実行を終了し、現在のロックを解放すると、所有者はnullに戻りますか?他のスレッドが取得できるように、カウントが1つ減ります。

第三に、ロックの最適化

ロックの解放とロック設定の取得には、オペレーティングシステムの下部でカーネルモードとユーザーモードの切り替えが含まれ、この切り替えは非常にリソースを消費します。ロックの取得と解放のパフォーマンス消費を削減するために、Jdk1.6ではこれらのロックが導入されています。 「バイアスロック」と「軽量ロック」です。これらのロックは、Synchronizedを使用するさまざまなアプリケーションシナリオで実際にはさまざまな状態です。MarkWordのロックフラグは、ロックの現在の状態も保存します。

画像-20210314161134325

ロックのアップグレードプロセスを上の図に示します。このアップグレードプロセスは元に戻せない、つまり矢印の方向にのみアップグレードできることに注意してください。一般的なプロセスは次のとおりです。

画像-20210314162037441

1.バイアスロック

オブジェクトヘッドが作成されMark Word(标记字段)Class Metadata Address(类型指针)スレッドがこのオブジェクトのモニターを保持すると、マークワードマークフィールドが読み取りロック01としてマークされ、バイアスモードにロックされます。

画像-20210314162914542

このモードでは、同じスレッドが低コストでのみロックを取得できます。つまり、シングルスレッドモードで使用する場合、同時実行性の高い環境でロックが閉じられると、複数のスレッドがロックをめぐって競合し始めます。そして、ロックは軽量ロックにアップグレードされます

2.軽量ロック

ロックを競合する前に、JVMは現在のスレッドのスタックフレームにロックレコード(ロックレコード)を格納するためのスペースを作成し、オブジェクトヘッダーのマークワードをロックレコードにコピーします。スレッドがロックを取得しようとすると、CASを使用してオブジェクトヘッダーのマークワードをロックレコードへのポインタに置き換えます。成功した場合はロックを取得します。失敗した場合は、他のリソースがロックを競合することを意味します。現在のスレッドは、スピンを使用してロックを取得しようとします。

画像-20210314164719709

3.スピンロック

ユーザーモードとカーネルモードを切り替えると、多くのリソースが消費されます。この消費を減らすために、JVMはスピンロックを導入します。リソースが取得されない場合、CPUを消費しますが、リソースを直接一時停止およびブロックするのではなく、スピンして待機します。リソースですが、この消費量はユーザーモードとカーネルモードの切り替えよりも少ないですが、スレッドがロックを取得できなかった場合、永久にスピンすることはできません。スピン数を制限する必要があります。スピン数がを超える場合10この後、ロックはウェイトロックにアップグレードされます

img

4.まとめ

いわゆるバイアスロックは好意を意味します。私は最初にロックを取得するスレッドを優先します。このロックは、シングルスレッド環境ではロックを取得するスレッドがないため、1つのスレッドのみの同期シナリオに適しています。仮想マシンもあります。操作をロックおよびロック解除する必要はありません。

ロックを取得する別のスレッドがある場合、バイアスされたロックが宣言されますが、現時点では、システムカーネル状態のステートメントを直接呼び出してロックするのではなく、CPUコードレベルで待機し、ロックが可能であることを期待しています。この方法で取得されます。これはスピンロックです。

10回以上回転してもロックが取得されない場合、CPUリソースを浪費するのを待つことができなくなり、ロックをウェイトロックにアップグレードして、ロックを競合するスレッドを一時的に停止します。

最初のスレッドがロックを取得するので、このロックは1つのスレッドのみの同期シナリオに適しています。これは、シングルスレッド環境では、ロックを取得するスレッドがなく、仮想マシンが実行する必要がないためです。ロックおよびロック解除操作。

ロックを取得する別のスレッドがある場合、バイアスされたロックが宣言されますが、現時点では、システムカーネル状態のステートメントを直接呼び出してロックするのではなく、CPUコードレベルで待機し、ロックが可能であることを期待しています。この方法で取得されます。これはスピンロックです。

10回以上回転してもロックが取得されない場合、CPUリソースを浪費するのを待つことができなくなり、ロックをウェイトロックにアップグレードして、ロックを競合するスレッドを一時的に停止します。

おすすめ

転載: blog.csdn.net/weixin_44706647/article/details/114945983