今日から、あなたとの同期の原則を詳しく学びます。原則部分には 2 つの記事が含まれます。
- バイアスされたロックを軽量ロックにアップグレードするプロセス
- 軽量ロックを重量ロックにアップグレードするプロセス
今日はまず、バイアスされたロックを軽量ロックにアップグレードするプロセスを学びます。多数の HotSpot ソース コードが関係しているため、ソース コードの注釈付きバージョンについては別の記事を用意します。
この記事を通じて、同期された質問について何に答えますか? 次の問題が統計にカウントされます。
- 同期の実装原理を詳しく説明する
- 同期がリエントラントロックであるのはなぜですか?
- 同期ロックのアップグレード(拡張)プロセスを詳しく説明します
- バイアスされたロックとは何ですか? synchronized はバイアスされたロックをどのように実装しますか?
- Java 8 以降、同期によりどのような最適化が行われましたか?
準備
同期されたソース コードの分析を正式に開始する前に、いくつかの準備をしましょう。
- HotSpot ソース コードの準備: JDK 11 を開きます。
- バイトコード ツール、jclasslib プラグインを推奨します。
- オブジェクトの状態を追跡するためのjol-coreパッケージ。
ヒント:
- javap コマンドと IDEA に付属のバイトコード ツールを使用できます。
- jclasslib の利点は、関連コマンドの公式サイトに直接ジャンプできることです。
サンプルコード
簡単なサンプルコードを準備します。
public class SynchronizedPrinciple {
private int count = 0;
private void add() {
synchronized (this) {
count++;
}
}
}
このツールを通じて、次のバイトコードを取得できます。
aload_0
dup
astore_1
monitorenter // 1
aload_0
dup
getfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
iconst_1
iadd
putfield #2 <com/wyz/keyword/synckeyword/SynchronizedPrinciple.count : I>
aload_1
monitorexit // 2
goto 24 (+8)
astore_2
aload_1
monitorexit // 3
aload_2
athrow
return
同期された変更されたコード ブロックは 2 つの命令にコンパイルされます。
- monitorenter : オブジェクトのモニターに入ります。
- monitorexit : オブジェクトのモニターを終了します。
monitorexit が 2 回表示されていることに気付きました。注 2 の部分はプログラムの正常な実行であり、注 3 の部分はプログラムの異常な実行です。Java チームはプログラムの異常事態まで考慮してくれて、本当に、死ぬほど泣きました。
ヒント:
- 同期された変更されたコード ブロックが例として使用される理由は、メソッドを変更するときに access_flag に ACC_SYNCHRONIZED フラグを設定するだけでは直感的ではないためです。
- Java は、monitorexit を通じてモニターを終了するだけではなく、かつては Unsafe クラスでモニターに出入りするメソッドを提供していました。
Unsafe.getUnsafe.monitorEnter(obj);
Unsafe.getUnsafe.monitorExit(obj);
Java 8 は使用可能ですが、Java 11 は削除されましたが、具体的な削除バージョンはわかりません。
jolの使用例
オブジェクトの状態はjol-coreによって追跡できます。Maven は以下に依存します。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
使用例:
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
モニターから開始Enter
HotSpot では、monitorenter コマンドは次の 2 種類の分析方法に対応します。
- バイトコードインタープリター: bytecodeInterpreter
- テンプレートインタープリター: templateTable_x86#monitorenter
bytecodeInterpreter は基本的に歴史の舞台から退いてしまったので、例としてテンプレート インタプリタ X86 を取り上げてtemplateTable_x86を実装します。
ヒント:
- 慣例により、ソース コードには主要な内容のみが表示されます。
- Yang Yi 先生の「Java 仮想マシン ホットスポットの詳細な分析」をお勧めします。
Monitorenter の実行メソッドはtemplateTable_x86#monitorenterです。このメソッドでは、 4438 行で実行される__lock_object(rmon)に注目し、interp masm_x86#lock_objectメソッドを呼び出すだけです。
void InterpreterMacroAssembler::lock_object(Register lock_reg) {
if (UseHeavyMonitors) {// 1
// 重量级锁逻辑
call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), lock_reg);
} else {
Label done;
Label slow_case;
if (UseBiasedLocking) {// 2
// 偏向锁逻辑
biased_locking_enter(lock_reg, obj_reg, swap_reg, tmp_reg, false, done, &slow_case);
}
// 3
bind(slow_case);
call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::monitorenter), lock_reg);
bind(done);
......
}
注 1 と注 2 の部分は、次の 2 つの JVM パラメータです。
// 启用重量级锁
-XX:+UseHeavyMonitors
// 启用偏向锁
-XX:+UseBiasedLocking
注 1 と注 3 の callInterpreterRuntime::monitorenter
メソッド。注 1 は重量ロックを直接使用する構成であるため、注 3 は偏ったロックの取得に失敗した後にロックを重量ロックにアップグレードするロジックであると推測できます。
オブジェクトヘッダー (markOop)
正式に始める前に、オブジェクトヘッダー ( markOop ) を理解しましょう。実際、markOop のコメントでその「秘密」が明らかになりました。
markOop はオブジェクトのヘッダーを記述します。
……
オブジェクト ヘッダーのビット形式 (最も重要なものが最初、以下はビッグ エンディアン レイアウト):
64ビット:
未使用:25 ハッシュ:31 -->| 未使用:1 経過時間:4 バイアスロック:1 ロック:2 (通常のオブジェクト)
JavaThread:54 エポック:2 未使用:1 経過時間:4 バイアスロック:1 ロック:2 (バイアスされたオブジェクト)
……
[JavaThread_ | エポック | 年齢 | 1 | 01] ロックは指定されたスレッドに偏っています
[0 | エポック | 年齢 | 1 | 01] ロックは匿名バイアスです
コメントでは、 64 ビットビッグ エンディアン モードでの Java オブジェクト ヘッダーの構造が詳細に説明されています。
ヒント:
- 32ビットmarkOopの構造も説明しましたが、突き抜けていませんでした~~
- markOop ロック フラグの列挙
オブジェクトヘッダーの構造のほとんどは理解するのが簡単ですが、エポックとは何でしょうか?
コメントでは、エポックは「バイアスされたロックのサポートに使用される」と説明されています。OpenJDK wiki の同期では、エポックについて次のように説明されています。
クラス内のエポック値は、バイアスの有効性を示すタイムスタンプとして機能します。
エポックはタイムスタンプに似ており、バイアスされたロックの有効性を示します。これは、バルク リバイアス フェーズ ( biasedLocking#bulk_revoke_or_rebias_at_safepoint ) 中に更新されます。
static BiasedLocking::Condition bulk_revoke_or_rebias_at_safepoint(oop o, bool bulk_rebias, bool attempt_rebias_of_object, JavaThread* requesting_thread) {
{
if (bulk_rebias) {
if (klass->prototype_header()->has_bias_pattern()) {
klass->set_prototype_header(klass->prototype_header()->incr_bias_epoch());
}
}
}
}
JVM はエポックを使用してバイアスされたロックに適しているかどうかを判断し、しきい値を超えると、JVM はバイアスされたロックをアップグレードします。JVM は、このしきい値を調整するためのパラメーターを提供します。
// 批量重偏向阈值
-XX:BiasedLockingBulkRebiasThreshold=20
// 批量撤销阈值
-XX:BiasedLockingBulkRevokeThreshold=40
ヒント: アップデートは klass の時代です。
バイアスロック (biasedLocking)
システムがバイアスされたロックを開くと、macroAssembler_x86#biased_locking_enterメソッドに入ります。このメソッドは最初にオブジェクトの markOop を取得します。
Address mark_addr (obj_reg, oopDesc::mark_offset_in_bytes());
Address saved_mark_addr(lock_reg, 0);
以下のプロセスを 5 つの分岐に分け、実行順序に従って偏ったロックの実装ロジックを一緒に分析します。
ヒント:
- バイアス ロックのプロセスを理解するだけで十分なので、図がメインであり、ソース コードの分析はバイアス ロックのソース コード分析に配置されます。
- バイアス ロックのソース コード分析は主にコメントに基づいており、各ブランチは詳細にマークされています。
- この部分には、実際にはundoとrebiasの2 つのジャンプ ラベルが含まれています。これらについては分岐図で説明します。
- ソースコードにはビットマスク技術が使用されており、区別しやすいように0Bから始まり4桁の2進数が埋め込まれています。
分岐 1: 偏見はありますか?
バイアスされたロックの前提条件のロジックは非常に単純で、現在のオブジェクト markOop のロック フラグを判断し、アップグレードされている場合はアップグレード プロセスを実行し、そうでない場合は下方向に実行を続けます。
ヒント: 点線のロジックは他のクラスにあります。
分岐 2: リエントラント バイアスはありますか?
現時点では、JVM は、markOop のロック フラグが 0B0101 であり、バイアスされた状態にあることを認識していますが、バイアスされているかどうかはまだ明らかではありません。HotSopt は、匿名を使用して、特定のスレッドにバイアスされる可能性はあるがバイアスされない状態を記述します。この状態は、匿名バイアスと呼ばれます。この時点でのオブジェクトヘッダーは次のようになります。
この時点で行うことは比較的単純で、現在のスレッドのバイアス ロックに再度入るかどうかを決定することです。リエントリーの場合はそのまま終了し、そうでない場合は下方向に実行を続けます。
ヒント: 今日、ある投稿を見つけました。Javaer と C++er は、リエントラント ロックと再帰的ロックについて議論しました。興味がある場合は、同時プログラミングにおけるロックを理解するための記事を読むことができます。リエントラント ロックと再帰的ロックの関係について簡単に説明しました。 。
分岐 3: それでも偏見はありますか?
コメントは、非リエントラントのバイアスされたロックの場合について説明しています。
この時点で、ヘッダーにバイアス パターンがあり、現在のエポックではバイアスの所有者ではないことがわかります。オブジェクトのヘッダーに対してどのような操作を合法的に実行できるかを知るために、ヘッダーの状態についてさらに詳細を把握する必要があります。
この時点で、次の 2 つの状況が考えられます。
- 競争は存在せず、スレッドは再びバイアスされます。
- レースがあります。元に戻してみてください。
部分ロックの取り消し部分は少し複雑で、オブジェクト klass の markOop を使用して、オブジェクトの markOop を置き換えます。主要なテクノロジはCASです。
分岐 4: エポックは期限切れですか?
バイアスされたロックの現在の状態はバイアス可能であり、他のスレッドに対してバイアスされています。この時点のロジックでは、フラグメント エポックが有効かどうかだけが必要です。
再バイアスは、CASを置き換えるために markOop を構築するという一文で説明できます。
ブランチ 5: 再バイアス
バイアスされたロックの現在のステータスは、他のスレッドにバイアスされる可能性があり、エポックの有効期限が切れていないことです。このとき行うことは、markOop に現在のスレッドを設定することです。これは、バイアス ロックを再バイアスするプロセスであり、分岐 4 の部分とよく似ています。
アンドゥとリビアス
バイアスされたロックの取得に失敗した後、interpreterRuntimeにある InterpreterRuntime::monitorenter メソッドを実行します。
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
if (UseBiasedLocking) {
// 完整的锁升级路径
// 偏向锁->轻量级锁->重量级锁
ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
} else {
// 跳过偏向锁的锁升级路径
// 轻量级锁->重量级锁
ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
}
IRT_END
ObjectSynchronizer::fast_enter に位置synchronizer.cpp#fast_enter:
void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
if (UseBiasedLocking) {
if (!SafepointSynchronize::is_at_safepoint()) {
// 撤销和重偏向
BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
return;
}
} else {
BiasedLocking::revoke_at_safepoint(obj);
}
}
// 跳过偏向锁
slow_enter(obj, lock, THREAD);
}
BiasedLocking::revoke_and_rebias
の要約コメント版は、バイアス ロック ソース コード分析のパート 2に配置されています。
軽量ロック(ベーシックロック)
バイアスされたロックの取得が失敗した場合は、この時点で実行されます。このメソッドはsynchronizer#slow_enterObjectSynchronizer::slow_enter
にあります。
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
// 无锁状态 ,获取偏向锁失败后有撤销逻辑,此时变为无锁状态
if (mark->is_neutral()) {
// 将对象的markOop复制到displaced_header(Displaced Mark Word)上
lock->set_displaced_header(mark);
// CAS将对象markOop中替换为指向锁记录的指针
if (mark == obj()->cas_set_mark((markOop) lock, mark)) {
// 替换成功,则获取轻量级锁
TEVENT(slow_enter: release stacklock);
return;
}
} else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
// 重入情况
lock->set_displaced_header(NULL);
return;
}
// 重置displaced_header(Displaced Mark Word)
lock->set_displaced_header(markOopDesc::unused_mark());
// 锁膨胀
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}
「The Art of Java Concurrent Programming」の軽量ロックのプロセスを直接引用します。
スレッドが同期ブロックを実行する前に、JVM はまず現在のスレッドのスタック フレームにロック レコードを保存するためのスペースを作成し、オブジェクト ヘッダーのマーク ワードをロック レコードにコピーします。これは正式には Displaced Mark Word と呼ばれます。 。次に、スレッドは CAS を使用して、オブジェクト ヘッダーのマーク ワードをロック レコードへのポインターに置き換えようとします。成功した場合は現在のスレッドがロックを取得しますが、失敗した場合は他のスレッドがロックを獲得するために競合し、現在のスレッドはスピンを使用してロックを取得しようとします。
軽量ロックのロジックは非常にシンプルで、使用される主要なテクノロジも CAS です。このときのmarkOopの構造は以下の通りです。
モニター出口で終了
バイアスされたロックまたは軽量ロックにある場合、monitorexit のロジックは非常に単純です。Monitorenter の経験があれば、monitorexit の呼び出しロジックを簡単に分析できます。
- templateTable_x86#monitorexit
- interp_masm_x86#un_lock
- ロック終了ロジック
- バイアスされたロック: macroAssembler_x86#biased_locking_exit
- 軽量ロック: interpreterRuntime#monitorexit
- ObjectSynchronizer#slow_exit
- ObjectSynchronizer#fast_exit
コードは皆さんが自分で調べられるように残されており、これが私の理解です。
通常、私は単純に、バイアスされたロックが終了するときは何もする必要はないと考えています (つまり、バイアスされたロックは積極的に解放されません)。軽量ロックの場合は、少なくとも 2 つの手順が必要です。
- displaced_header をリセットします。
- ロックレコードを解放します。
したがって、終了ロジックの観点から見ると、軽量ロックのパフォーマンスは、バイアスされたロックのパフォーマンスよりもわずかに劣ります。
要約する
この段階の内容を簡単に要約すると、バイアスされたロックと軽量ロック、特に軽量ロックのロジックは複雑ではありません。
バイアスされたロックと軽量ロックの主要なテクノロジーは CAS です。CAS 競合が失敗すると、他のスレッドがそれを奪い取ろうとすることになり、ロックのアップグレードにつながります。
バイアスされたロックは、初めてロックを保持したスレッドをオブジェクト markOop に記録します。スレッドがバイアスされたロックを保持し続ける場合、必要なのは単純な比較のみです。ほとんどのシナリオとシングルスレッド実行に適していますが、場合によっては、スレッド競争のシナリオかもしれません。
しかし問題は、スレッドが交互に保持されて実行される場合、バイアスされたロックの取り消しと再バイアスのロジックが複雑になり、パフォーマンスが低下することです。したがって、このような「わずかな」競合状態が交互に起こる場合の安全性を確保するために、軽量ロックが導入されました。
さらに、偏ったロックについては、主に次の 2 つの点で多くの論争があります。
- バイアスされたロックを取り消すと、パフォーマンスに大きな影響を与えます。
- 大量の同時実行がある場合、偏ったロックは非常に悪趣味です。
実際、バイアスされたロックは Java 15 で廃止されました ( JEP 374: バイアスされたロックの廃止と無効化)、しかし、ほとんどのアプリケーションは依然として Java 8 で実行されているため、バイアスされたロックのロジックを理解する必要があります。
最後に、噂 (または顔を平手打ちされる?) に反論しましょう。軽量ロックにはスピン ロジックはありません。
ヒント: バッチの取り消しとバッチのリダイレクトが見逃されているようです~~
この記事が役に立った場合は、たくさんの賞賛とサポートをお願いします。記事に間違いがあった場合は、批判と修正をお願いします。最後に、皆さんもぜひ、ハードコア Java テクノロジーを共有する金融マン。