パフォーマンスと引き換えに、JVM は組み込みロックに対して多くの最適化を行っており、拡張ロック割り当て戦略はその 1 つです。バイアス付きロック、軽量ロック、重量ロックの解決すべき基本的な問題、およびいくつかのロックの割り当てと拡張プロセスを理解することは、ロックベースの並行プログラムを作成して最適化するのに役立ちます。
組み込みロックの割り当てと拡張プロセスは比較的複雑で、時間とエネルギーによって制限されます. 記事のこの部分は、インターネット上の複数のソースの統合に基づいています. 参照の便宜のために, 続行するときの参照もあります.後で JVM ソース コードを分析します。すべてのレベルでロックの基本を既に理解している場合は、この記事を読み飛ばしてかまいません。
1 組み込みのロックの下に隠された根本的な問題
組み込みロックは、JVM が提供する最も便利なスレッド同期ツールです。組み込みロックは、コード ブロックまたはメソッド宣言に synchronized キーワードを追加することで使用できます。組み込みロックを使用すると、同時実行モデルを簡素化できます。JVM をアップグレードすると、コードを変更することなく、組み込みロックで JVM の最適化結果を直接享受できます。単純な重量ロックから徐々に拡大するロック割り当て戦略まで、さまざまな最適化手法を使用して、組み込みロックの下に隠れている基本的な問題を解決します。
1.1 ヘビーロック
組み込みロックは、Java のモニター ロックとして抽象化されます。JDK 1.6 より前では、モニター ロックは、基盤となるオペレーティング システムのミューテックスに直接対応していると考えることができました。システム コールによるカーネル モードとユーザー モードの切り替え、スレッド ブロックによるスレッド切り替えなど、この同期方法のコストは非常に高くなります。そのため、このロックは後に「重量ロック」と呼ばれるようになりました。
1.1.1 スピンロック
まず、カーネル モードとユーザー モードの切り替えを最適化するのは簡単ではありません。ただし、スピン ロックを使用すると、スレッドのブロックによって発生するスレッドの切り替え(スレッドの中断とスレッドの再開を含む)を減らすことができます。
ロックの粒度が小さい場合、ロックの保持時間は比較的短いです(具体的な保持時間はわかりませんが、通常、上記の特性を満たすことができるロックはいくつかあると考えられます)。そして、これらのロックを奪い合う者にとっては、ロック閉塞によるスレッド切り替え時間はロック保持時間に相当するため、スレッド閉塞によるスレッド切り替えを減らすことで大幅な性能向上を実現できます。詳細は次のとおりです。
現在のスレッドがロックの競合に失敗すると、それ自体をブロックしようとします。
自分自身を直接ブロックするのではなく、しばらくスピン (空の制限された for ループなどの空の待機) を行います。
回転中の再競合ロック
スピンの終了前にロックが取得された場合、ロックの取得は成功です; そうでない場合は、スピンの終了後に自身をブロックします
スピン時間内に古い所有者によってロックが解放された場合、現在のスレッドはそれ自体をブロックする必要がなく(また、将来ロックが解放されたときに回復する必要もありません)、1 つのスレッド切り替えが削減されます。
「比較的短時間ロックを保持している」状態を緩和できます。実際、ロックの競合時間が比較的短い場合 (たとえば、スレッド 1 がロックを解放しようとすると、スレッド 2 がロックを競合するようになる)、スピンによってロックを取得する確率を高めることができます。これは通常、ロックが長時間保持されているシナリオで発生しますが、競争は激しいものではありません。
欠点:
シングルコア プロセッサでは、実際の並列性はありません.現在のスレッドがそれ自体をブロックしない場合、古い所有者は実行できず、ロックは決して解放されません.このとき、スピンがどれだけ長くても、それはさらに、スレッド数が多く、プロセッサ数が少ない場合、スピンによって多くの不要な無駄が発生します。
スピン ロックは CPU を占有します. 計算量の多いタスクの場合, この最適化は通常, ろうそくの価値はありません. ロックの使用を減らすことがより良い選択です.
ロック競合時間が比較的長い場合、通常、スピンはロックを取得できず、スピンが占有する CPU 時間が浪費されます。これは通常、ロックが長時間保持され、競争が激しいシナリオで発生します. このとき、スピンロックを積極的に無効にする必要があります.
-XX:-UseSpinning パラメータを使用してスピンロックの最適化をオフにし、-XX:PreBlockSpin パラメータを使用してデフォルトのスピン数を変更します。
1.1.2 適応スピン
適応とは、スピン時間がもはや固定されていないことを意味しますが、同じロックでの前回のスピン時間とロック所有者の状態によって決定されます。
同じロック オブジェクトで、スピン待機がロックの取得に成功し、ロックを保持しているスレッドが実行中の場合、仮想マシンはこのスピンが再び成功する可能性が高いと判断し、スピン待機を許可します。比較的長い時間、たとえば 100 ループ。
反対に、特定のロックに対してスピンがほとんど正常に取得されない場合は、プロセッサ リソースの浪費を回避するために、将来ロックを取得するときにスピン時間を短縮するか、スピン プロセスを省略することもできます。 .
アダプティブスピンが「不確定なロック競技時間」の問題を解決。
JVM が正確なロック競合時間を認識することは困難であり、分析のためにそれをユーザーに引き渡すことは、JVM の当初の設計意図に違反します。アダプティブ スピンは、異なるスレッドが同じロック オブジェクトをほぼ同時に保持することを前提としており、競合の度合いが安定する傾向があるため、最後のスピンの時間と結果に応じて、次のスピンの時間を調整できます。
欠点:
ただし、アダプティブ スピンではこの問題を完全に解決することはできず、デフォルトのスピン数の設定が不当 (高すぎたり低すぎたり) であると、適応プロセスが適切な値に収束することが困難になります。
1.2 軽量ロック
スピンロックの目的は、スレッド切り替えのコストを削減することです。ロック競合が激しい場合、競合に失敗したスレッドをブロックするために重いロックに頼らなければなりません; 実際のロック競合がまったくない場合、重いロックを申請するのは無駄です。軽量ロックの目的は、システム コールによるカーネル モードとユーザー モードの切り替え、スレッド ブロックによるスレッド切り替えなど、実際の競合なしに重いロックを使用することによるパフォーマンスの消費を削減することです。
名前が示すように、軽量ロックは重量ロックに関連しています。軽量ロックを使用する場合、ミューテックスを申請する必要はなく、_Mark Word 内のバイト CAS の部分をスレッド スタック内のロック レコードに更新するだけで、更新が成功した場合、軽量ロックが正常に取得_され、記録されます。ロック状態 軽量ロックです;それ以外の場合は、スレッドが既に軽量ロックを取得しており、現在ロック競合が発生しており (軽量ロックを使用し続けるのは適切ではありません)、その後、重量ロックに拡張されます。 .
Mark Word はオブジェクト ヘッダーの一部であり、各スレッドには独自のスレッド スタック (仮想マシン スタック) があり、スレッドと関数呼び出しの基本情報が記録されます。この 2 つは JVM の基本的な内容に属し、ここでは紹介しません。
もちろん、軽量ロックは当然ロック競合のないシナリオを対象としているため、ロック競合はあるが激しいものではない場合でも、スピン ロックを使用して最適化し、スピンが失敗した後に重量ロックに拡張することができます。
欠点:
スピンロックに似ています:
ロック競争が激しい場合、軽量ロックはすぐに重量ロックに拡大し、軽量ロックを維持するプロセスは無駄になります。
1.3 バイアスロック
実際の競争がない場合でも、一部のシナリオでは引き続き最適化できます。実際の競合がないだけでなく、最初から最後までロックを使用するスレッドが 1 つしかない場合、軽量ロックを維持するのは無駄です。バイアス ロックの目的は、競合がなく、ロックを使用するスレッドが 1 つだけの場合に、軽量ロックを使用することによるパフォーマンスの消費を減らすことです。軽量ロックは、ロックを適用して解放するたびに少なくとも 1 つの CAS を必要としますが、バイアス ロックは初期化時に 1 つの CAS しか必要としません。
「バイアス」とは、バイアスをかけられたロックが、ロックを適用する最初のスレッドのみが将来ロックを使用することを前提としていることを意味します(スレッドが再びロックを適用することはありません) 。 Word (基本的には更新も行います. ただし初期値は空です), 記録が成功した場合, バイアスロックの取得に成功し,記録されたロック状態がバイアスロックである.今後, 現在のスレッドは所有者と等しくなります.ロックはゼロ コストで直接取得できます。それ以外の場合は、他のスレッドが競合していることを意味し、展開は軽量レベルの lock です。
偏ったロックは、スピン ロックを使用して最適化することはできません。これは、他のスレッドがロックを適用すると、偏ったロックの仮定が崩れるためです。
欠点:
同様に、明らかに他のスレッドがロックを申請している場合、バイアスされたロックはすぐに軽量ロックに拡張されます。
ただし、この副作用ははるかに小さくなっています。
必要に応じて、パラメーター -XX:-UseBiasedLocking (デフォルトでオン) を使用してバイアス ロックの最適化を無効にします。
1.4 まとめ
バイアスロック、軽量ロック、重量ロックの割り当てと拡張の詳細なプロセスについては、以下を参照してください。これには、Mark Word と CAS に関するある程度の知識が必要です。
バイアス ロック、軽量ロック、および重量ロックは、さまざまな同時実行シナリオに適しています。
バイアスされたロック: 実際の競合はなく、ロックを申請した最初のスレッドのみが将来的にロックを使用します。
軽量ロック: 実際の競合はなく、複数のスレッドがロックを交互に使用します; 短期間のロック競合は許可されます。
重量級ロック:実戦あり、ロック競技時間が長い。
さらに、ロックの競合時間が短い場合は、スピン ロックを使用して軽量ロックと重量ロックのパフォーマンスをさらに最適化し、スレッドの切り替えを減らすことができます。
ロック競合の程度が徐々に (ゆっくりと) 増加する場合、バイアス ロックから加重ロックに徐々に拡張することで、システムの全体的なパフォーマンスを向上させることができます。
2 ロックの割り当てと拡張プロセス
繰り返しますが、この部分は主にインターネット上の複数の情報源に基づいて編集されています。中核となるのは、この 巨人 、これは非常に詳細で基本的に論理的です。
組み込みロックの使用におけるいくつかの基本的な問題と解決策を上で説明し、実装の原則について簡単に説明しました。詳細なロック割り当てと拡張プロセスは次のとおりです。
図で質問があります。
図のフローに従って、 ロックが重量ロックに拡張されていることが判明した場合、現在のスレッドは直接ミューテックスでブロックされます 。
ただし、スピンロックの大きな利点の 1 つは、スレッド切り替えのオーバーヘッドを削減できることです。ここで現在のスレッドを直接ブロックする必要はありません。軽量ロックのように、しばらくスピンしてから、失敗するとブロックすることができます。
特に2つのポイント:
CAS が所有者を記録するとき、== nullおよび newValue == ownerThreadId が期待される. したがって、バイアス ロックを適用する最初のスレッドのみが成功を返すことができ、後続のスレッドは必然的に失敗します(一部のスレッドはバイアスを検出し、その時点で所有者を CAS に記録しようとします)。同時)。
組み込みロックは、バイアス ロック、軽量ロック、重量ロックの順に徐々に拡張することしかできず、「縮小」することはできません。これは、JVM のもう 1 つの前提である「上位レベル ロックの前提が崩れると、将来的にはその前提が成り立たなくなると考えられる」に基づいています。
また、重量ロックを解除する際に、ブロックされていたスレッドを起こさなければならないというロジックは、基本的に ReentrantLock と同じです。
上の図を単純化すると、次のようになります。