Javaマルチスレッドのsynchronizedとその最適化

7248 ワード

Synchronizedと同期ブロック

synchronizedはjvmによって提供される同期およびロック機構であり、これに対応してjdkレベルのJ.U.Cによって提供されるAbstractQueuedSynchronizerに基づく同時コンポーネントである.synchronizedは反発同期を提供し、反発同期とは、複数のスレッドが共有データに同時にアクセスする場合に、共有データが同じ時刻に1つのスレッドしかアクセスしないことを保証することを意味する.jvmでは、synchronizedで修飾されたコードブロックがjavacでコンパイルされると、ロックおよびロック解除するオブジェクトを指定するために、コードブロックの前後にmonitorenterおよびmoniterexitバイトコード命令がそれぞれ生成されます.synchronizedでオブジェクトパラメータが指定されている場合、referenceはそのオブジェクトの参照であり、手動で指定されていない場合は、synchronizedに従ってインスタンスメソッドかクラスメソッドかを修飾し、対応するオブジェクトインスタンスまたはClassオブジェクトをロックオブジェクトとして取得します.特筆すべきは、javaにおけるObjectクラスには、wait()およびnotify()(notifyAll()notify()と類似)の2つの方法と同期ロックが関連している.この2つの方法がなぜObjectクラスに配置されるのかについては、wait()メソッドの意味は、現在のスレッド(wait()メソッドを呼び出すスレッド)を待つことであり、notify()メソッドによって呼び出されることを知っているからである.現在のスレッドにはオブジェクトのロックが必要です.wait()メソッドを呼び出すと、現在のスレッドは同期オブジェクトのロックを解放します.したがって、wait()メソッドは、synchronizedによって修飾されたコードブロック内(同期ロックが必ず取得される)でなければならない.いずれのJavaオブジェクトもオブジェクトロックとして機能するため、この2つのメソッドはObjectに配置する必要がある.Javaスレッドはオペレーティングシステムのオリジナルスレッドにマッピングされており、スレッドをブロックまたは起動するには、カーネルにアクセスして完了する必要があります.このようなユーザー状態からカーネル状態への状態変換には多くのプロセッサ時間がかかるため、synchronizedはJavaで非常にリソースを消費する操作です.JDK 1.5以前、synchronizedはJDKベースのReentrantLockに比べて性能がかなり低かった.JDK 1.6以降のバージョンでは、synchronizedはロックに対する多くの最適化措置を実現し、これらの最適化の大部分はReentrantLockの実現構想と似ている.

非ブロック同期とCAS


ブロック同期は、常にロックの追加とロックの解除に関連します.これは、ユーザー状態とカーネル状態の切り替えを意味し、リソースを非常に消費します.このため、衝突検出に基づく楽観的な同時戦略が誕生した.この戦略の核心思想は、まずデータを操作し、他のスレッドがデータを競合しなければ、操作が成功したと考えている.そうでなければ、データ操作の成功を保証するために他の方法を採用します(たとえば、再試行し続け、成功を知っています).この同時ポリシーは、スレッドを一時停止する必要がないので、非ブロック同期と呼ばれます.CASはCompareAndSwapの略称で、メモリ位置、予想値、更新値の3つのオペランドが必要です.CAS命令が実行されると、プロセッサはまずメモリ位置の値が予想値と等しいかどうかを判断し、等しい場合、予想値を更新値に更新する.そうでなければ、他のスレッドがこのメモリ位置の値を変更したと考えられ、更新操作は許可されません.更新操作が発生するかどうかにかかわらず、CAS操作は予想値を返し、CAS操作は原子操作です.CASは非ブロック同期に非常に適しており、例えばint型の値iに1を加えるには、変数iのメモリ位置の値が変更されているかどうかを判断し、そうでなければ、この位置の値をi+1に更新することを考えている.そうでなければ、他のスレッドがこの値を変更したと考えられ、更新が成功するまで、前の操作を再呼び出して更新を試みることができます.なぜなら,メモリ位置の値を判断する際にはまずこのメモリ位置の値を取得する必要があり,Javaでは,この変数の可視性を保証するためにvolatileキーワードで修飾する必要があるからである.JDK 1.5後、Javaプログラムはsun.misc.Unsafeクラスの基本タイプのCAS操作を実現したパッケージを提供し、JUCコンポーネントはこのクラスに基づいて実現された.java.util.concurrent.atomic.AtomicIntegerクラスを例にとると、このクラスにはインスタンス変数valueがパッケージされています.
private volatile int value;

この変数の自己増加を実現するソースコードは、次のとおりです.
/**
  * Atomically increments by one the current value.
  *
  * @return the updated value
  */
  public final int incrementAndGet() {
      return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
  }

そのうち、Unsafe#getAndAddInt()の実装は以下の通りである.
public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

ここでvar 2はメモリアドレスの値であり、var 5は予想値であり、compareAndSwapInt()メソッドがtrueを返すと、このCAS操作が成功したことを示し、そうでなければ再試行し、成功するまで知る.ここから、CAS操作の3つの欠点を発見することができる.
  • ABAの問題は、変数の値が他のスレッドによって変更されたが、現在のスレッドがCAS命令を呼び出す前に予想値に更新された場合、CAS命令は依然として内位置の値が変更されていないと考えられ、CAS設計の意図と一致しないことである.しかし、バージョン番号を導入することでこの問題を解決することができます.
  • 効率の問題.指定した変数の値を変更するスレッドが多数存在する場合、CAS操作が成功する確率は低く、再試行を続けると仮想マシンのパフォーマンスが低下します.
  • CASは依然として単一変数の同期しかできず、synchronizedのようにコードの同期を実現できない.ここで、unsafeはUnsafeの例で、このクラスはユーザープログラムに提供されるクラスではありません.その中にパッケージされているのはほとんどJNIメソッドです.プログラマーは一般的にこのクラスを使用する必要はありません.JavaのCAS操作のパッケージであることを知っていればいいだけです.

  • ロックの最適化


    前述のsynchronizedに対応する同期ロックは比較的重量級であるため、JDK 1.5以降、開発者は、適応スピンロック、ロック消去、ロック粗化、軽量ロック、偏向ロックなど、様々なロック最適化技術を実現した.なお、これらのロックはいずれも仮想マシンレベルの最適化であり、synchronizedに対応するバイトコードの最適化と考えられる.

    スピンロックとアダプティブスピンロック


    スピンロックは仮想マシン開発者の共有データの統計分析から得られた最適化技術であり、多くの場合、共有データのロック状態は短い時間しか続かないが、他のスレッドはロックを待っている間に、この待機時間のためにスレッドを停止し、回復する価値はない.スレッドを保留せずに少し待つために、スレッドに忙しいサイクル(スピン)を実行させることができます.これがスピンロックです.なお、スピンロックはCPUリソースを消費する必要があり、たまたま共有データのロック時間が長いと、逆にスピンロックの性能が低下する.適応スピンロックはスピンロックの最適化であり,盲目的な待機を避けるためにスピン時間を動的に調整することができる.

    ロック解除


    ロック除去はjavacレベルでの最適化です.一部のプログラマが作成したsynchronizedで修飾されたコードを呼び出すか呼び出すが、共有データ競合が存在しないことが検出された場合、javacはこのコードを最適化し、余分な同期命令を除去する.

    ロック粗化


    原則として、プログラムを作成する際には、同期ブロックが小さいほど良いことが要求されるが、一連の連続操作が同じオブジェクトに対する反復ロックとロック解除である場合、リソースの浪費であり、この場合、ロックの範囲を操作シーケンス全体に拡大することは、反復ロックとロック解除の占有リソースを節約するのに有利である.ロック除去およびロック粗化技術はjavacまたはjvmのインテリジェントな最適化である.

    軽量ロック


    軽量ロックは、マルチスレッド競合がないことを前提として、従来の重量ロックがオペレーティングシステムの反発量を使用して発生するパフォーマンス消費を低減します.軽量ロックの実現根拠は、「ほとんどの場合、同期サイクル全体で競合は存在しない」ことであり、競合が存在しない場合、軽量ロックを使用するにはオペレーティングシステムの反発量を使用する必要がなく、リソースを節約できます.しかしながら、競合が存在する場合、軽量レベルロックにはCASオーバーヘッドと従来の重量レベルロック動作のオーバーヘッドが存在し、性能がより低くなる.軽量ロックの実装には、ロックとロック解除の2つのプロセスがあります.
  • ロック:jvmは、現在のスレッドのスタックフレームにロックレコード(Lock Record)という空間を確立し、ロックオブジェクトのオブジェクトヘッダ情報を格納する.ストレージが成功すると、仮想マシンはCAS操作を使用してロックするオブジェクトのオブジェクトヘッダをロックレコードへのポインタに更新しようとします.この操作が成功すると、現在のスレッドがオブジェクトのロックを持っていると考えられます.このとき、オブジェクトヘッダのロックフラグビットは「00」-軽量レベルのロックに更新されます.失敗した場合、このスレッドがすでにこのオブジェクトのロックを所有しているか、共有データが競合しているかの2つの状況があり、このロックがすでに所有されているかどうかを判断するには、ターゲットオブジェクトのMark Wordが現在のスタックフレームを指しているかどうかを確認する必要があり、もしそうであれば、直接同期に入るのが速い.そうでなければ、競争があると考えられ、軽量級ロックを重量級ロックに更新し、その後、従来の重量級ロック操作と同様になった.
  • アンロック:アンロックもCAS操作に基づいて実現される.ロック解除時、オブジェクトのMark Wordが現在のスレッドスタックフレームを指している場合は、CASでオブジェクトのMark Wordを更新します.成功すると、同期プロセス全体が完了します.そうでなければ、ロックを取得しようとする他のスレッドがあることを示します.ロックを解除しながら、保留中のスレッドを起動します.

  • バイアスロック


    バイアスロックは軽量レベルのロックに比べてより急進的な方法である:競合なしに同期全体を除去し、バイアスロックはロックが最初に取得したスレッドに偏っていることを意味し、次の実行中にロックが他のスレッドに取得されなければ、バイアスロックを持つスレッドは同期を必要としない.仮想マシンは-XX:+UseBiasedLockingパラメータを使用してヨーロックを開始し、ロック対象がスレッドによって初めて取得されると、仮想マシンはオブジェクトヘッダのフラグビットを「01」--ヨーロックモードに設定します.同時にCAS操作を用いてこのロックを取得したスレッドのIDをオブジェクトのMark Wordに記録し、CAS操作が成功すれば、偏向ロックを持つスレッドがこのロックに関連する同期ブロックに入るたびに、仮想マシンは同期操作を行わなくてもよい.別のスレッドがこのロックを取得しようとすると、バイアスロックは終了します.このとき,ロック対象が現在ロック状態にあるか否かにより,偏向を解消して未ロックまたは軽量ロックの状態に復帰し,後続の同期動作は前述した軽量ロックのように実行される.
    参考:「Java仮想マシンを深く理解する」第2版、周志明