synchronized の実装原理#
synchronized は、メソッドまたはコードブロックが実行中に、同時に 1 つのメソッドだけがクリティカルセクションに入ることを保証し、共有変数のメモリの可視性も保証します。
Java オブジェクトヘッダーと monitor#
Java オブジェクトヘッダーと monitor は、synchronized を実現するための基礎です。
オブジェクトヘッダー#
Hotspot 仮想マシンのオブジェクトヘッダーは、主に 2 つのデータを含みます:Mark Word(マークワード)、Klass Pointer(クラスポインタ)
。その中でKlass Pointer
は、オブジェクトがそのクラスのメタデータを指すポインタであり、仮想マシンはこのポインタを使用してこのオブジェクトがどのクラスのインスタンスであるかを特定します。Mark Word
は、オブジェクト自身の実行時データを格納するために使用され、例えばハッシュコード(HashCode)
、GC世代年齢
、ロック状態フラグ
、スレッドが保持しているロック
、偏向スレッドID
、偏向タイムスタンプ
などが含まれます。これは軽量ロックと偏向ロックを実現するための重要な要素です。
参考リンク:
monitor#
オペレーティングシステムのロックを連想してください。
作用範囲#
Java では、各オブジェクトがロックとして機能でき、これは synchronized が同期を実現する基礎です:
インスタンスメソッドを修飾する
場合、ロックは現在のインスタンスオブジェクトです。静的メソッドを修飾する
場合、ロックは現在のクラスの class オブジェクトです。コードブロックを修飾する
場合、ロックは括弧内のオブジェクトです。
同期コードブロックはmonitorenter
とmonitorexit
命令を使用して実現され、同期メソッドはメソッド修飾子のACC_SYNCHRONIZED
によって実現されます。
マルチスレッドの競合時のロックの状況分析#
ロックオブジェクト#
状況 1:
同じオブジェクトが 2 つのスレッドでそれぞれ異なる synchronized 同期メソッドにアクセスする。
結果:相互排他が発生します。
説明:ロックはオブジェクトに対して行われるため、オブジェクトが synchronized メソッドを呼び出すと、他の同期メソッドはその実行が終了しロックが解放されるまで待機する必要があります。
状況 2:
異なるオブジェクトが 2 つのスレッドで同じ synchronized 同期メソッドを呼び出す。
結果:相互排他は発生しません。
説明:異なるオブジェクトであるため、ロックはオブジェクトに対して行われ、メソッドに対してではないため、並行して実行できます。言い換えれば、各スレッドがメソッドを呼び出すときに新しいオブジェクトを生成するため、2 つのスペースと 2 つの鍵が存在します。
クラスロック#
状況 1:
クラスを使用して 2 つのスレッドで異なる synchronized 同期メソッドを呼び出す。
結果:相互排他が発生します。
説明:クラス(.class)にロックをかけると、クラスオブジェクトは 1 つしか存在せず、常に 1 つのスペースしか存在しないと理解できます。その中に N 個の部屋があり、1 つのロックがあるため、部屋(同期メソッド)間は必ず相互排他になります。
注:上記の状況は、シングルトンパターンを使用してオブジェクトを宣言し、非静的メソッドを呼び出す場合と同じです。なぜなら、常にこの 1 つのオブジェクトしか存在しないからです。したがって、同期メソッド間は必ず相互排他になります。
状況 2:
あるクラスの静的オブジェクトを使用して 2 つのスレッドで静的メソッドまたは非静的メソッドを呼び出す。
結果:相互排他が発生します。
説明:同じオブジェクトが呼び出されるため、状況 1 と同じです。
状況 3:
1 つのオブジェクトが 2 つのスレッドでそれぞれ静的同期メソッドと非静的同期メソッドを呼び出す。
結果:相互排他は発生しません。
説明:同じオブジェクトが呼び出されますが、2 つのメソッドのロックタイプが異なるため、呼び出される静的メソッドは実際にはクラスオブジェクトによって呼び出されており、これら 2 つのメソッドが生成するのは同じオブジェクトロックではないため、相互排他は発生せず、並行して実行されます。
ロックの最適化#
jdk1.6 では、ロックの実装に多くの最適化が導入されました。例えば、自旋ロック、適応的自旋ロック、ロック消去、ロック粗化、偏向ロック、軽量ロックなどの技術を使用してロック操作のオーバーヘッドを削減します。ロックは主に 4 つの状態があり、順に:無ロック状態、偏向ロック状態、軽量ロック状態、重量ロック状態です。これらは競争が激しくなるにつれて徐々にアップグレードされます。注意:ロックはアップグレード可能ですが、ダウングレードはできません。この戦略は、ロックの取得と解放の効率を向上させるためのものです。
ロック消去#
データの完全性を保証するために、操作を行う際にはこの部分の操作を同期制御する必要がありますが、場合によっては JVM が共有データ競争が存在しないことを検出すると、JVM はこれらの同期ロックをロック消去します。その根拠は、エスケープ分析のデータサポートです。
ロック粗化#
一連の連続したロックとアンロック操作は、不要なパフォーマンス損失を引き起こす可能性があるため、ロック粗化の概念が導入されました。これは、複数の連続したロックとアンロック操作を結合し、より広い範囲のロックに拡張することを意味します。
自旋ロック#
スレッドのブロックとウェイクアップには、CPU がユーザーモードからカーネルモードに切り替わる必要があります。頻繁なブロックとウェイクアップは CPU にとって非常に重い負担であり、システムの並行性能に大きな圧力をかけることになります(連想記憶 ゼロコピー
も不要な切り替えを減らすためのものです)。
自旋ロックとは何ですか?自旋ロックとは、スレッドがしばらく待機し、すぐに中断されずにロックを保持しているスレッドがすぐにロックを解放するかどうかを確認することです。どうやって待機するかというと、無意味なループを実行するだけです(自旋)。
自旋待機はブロックの代わりにはなりません。プロセッサの数に関する要件(マルチコア、現在はシングルコアのプロセッサは存在しないようです)を考慮しなくても、スレッド切り替えによるオーバーヘッドを回避できますが、プロセッサの時間を消費します。もしロックを保持しているスレッドがすぐにロックを解放した場合、自旋の効率は非常に良いですが、逆に自旋しているスレッドは処理リソースを無駄に消費し、何の意味のある作業も行わないため、典型的には「トイレを占有しているが用を足さない」状態になり、パフォーマンスの無駄を引き起こします。
適応自旋ロック#
JDK 1.6 では、より賢い自旋ロック、すなわち適応自旋ロックが導入されました。適応とは、自旋の回数が固定されていないことを意味し、前回同じロックでの自旋時間とロックの所有者の状態によって決まります。スレッドが自旋に成功した場合、次回の自旋回数は増加します。逆に、特定のロックに対して自旋が成功することがほとんどない場合、今後そのロックを取得する際の自旋回数は減少し、場合によっては自旋プロセスを省略してプロセッサリソースの無駄を避けます。
軽量ロック#
軽量ロックの性能向上の根拠は「ほとんどのロックはそのライフサイクル全体で競争が存在しない」ということです。この根拠が破られると、相互排他のオーバーヘッドに加えて、追加の CAS 操作が発生します。したがって、マルチスレッド競争の状況では、軽量ロックは重量ロックよりも遅くなります。
偏向ロック#
軽量ロックのロックとアンロック操作は、複数回の CAS 原子命令に依存する必要があります。偏向ロックは、Mark Word が偏向状態であるかどうかを検出し、そうであれば CAS 操作をスキップして直接同期コードブロックを実行することで性能を向上させます。
重量ロック#
重量ロックは、オブジェクト内部のモニター(monitor)を通じて実現されます。その本質は、基盤となるオペレーティングシステムの Mutex Lock の実装に依存しています。オペレーティングシステムがスレッド間の切り替えを実現するには、ユーザーモードからカーネルモードへの切り替えが必要であり、切り替えコストは非常に高いです。