今日も同期アップグレード プロセスの学習を続けますが、残っているのは最後のステップ、軽量ロック -> 重量ロックのみです。
今日のコンテンツを通じて、同期に関するすべての質問に答えることができれば幸いです。ロックの粗密化、ロックの削除、および Java 8 の同期の最適化に加えて、すべての問題があります。
重量級のロックを取得する
偏ったロックのアップグレードをソース コードからわかりやすく説明する最後に、synchronizer#slow_enterに競合がある場合、 ObjectSynchronizer::inflate メソッドを呼び出して、軽量ロックをアップグレード (インフレート) します。
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
......
ObjectSynchronizer::inflate(THREAD, obj(), inflate_cause_monitor_enter)->enter(THREAD);
}
ObjectSynchronizer::inflate を通じてヘビーウェイト ロック ObjectMonitor を取得し、ObjectMonitor::enter メソッドを実行します。
ヒント:
- この方法は、スレッドについて知っておくべき 8 つの質問 (中)で説明されています。
- 問題はロックのエスカレーション (拡張) ですが、そこには焦点が当てられていない
ObjectSynchronizer::inflate
ため、コード分析はヘビーウェイト ロックのソース コード分析に配置されます。
ロック構造
ObjectMonitor::enter のロジックを理解する前に、まずObjectMonitorの構造を見てください。
class ObjectMonitor {
private:
// 保存与ObjectMonitor关联Object的markOop
volatile markOop _header;
// 与ObjectMonitor关联的Object
void* volatile _object;
protected:
// ObjectMonitor的拥有者
void * volatile _owner;
// 递归计数
volatile intptr_t _recursions;
// 等待线程队列,cxq移入/Object.notify唤醒的线程
ObjectWaiter * volatile _EntryList;
private:
// 竞争队列
ObjectWaiter * volatile _cxq;
// ObjectMonitor的维护线程
Thread * volatile _Responsible;
protected:
// 线程挂起队列(调用Object.wait)
ObjectWaiter * volatile _WaitSet;
}
_header フィールドにはオブジェクトの markOop が格納されますが、なぜこのようにする必要があるのでしょうか? ロックがアップグレードされた後は Object の markOop を保存するスペースがないため、終了時にロック前の状態に復元できるように _header に保存されます。
ヒント:
- 実際、basicLock はオブジェクトの markOop も保存します。
- EntryList 内の待機中のスレッドは、cxq の移動によって発生するか、Object.notify が起動しますが実行されません。
リエントラント性の実装
objectMonito#enterメソッドは 3 つの部分に分けることができます。最初の部分は、競争または再エントリーが成功したシーンです。
// 获取当前线程Self
Thread * const Self = THREAD;
// CAS抢占锁,如果失败则返回_owner
void * cur = Atomic::cmpxchg(Self, &_owner, (void*)NULL);
if (cur == NULL) {
// CAS抢占锁成功直接返回
return;
}
// CAS失败场景
// 重量级锁重入
if (cur == Self) {
// 递归计数+1
_recursions++;
return;
}
// 当前线程是否曾持有轻量级锁
// 可以看做是特殊的重入
if (Self->is_lock_owned ((address)cur)) {
// 递归计数器置为1
_recursions = 1;
_owner = Self;
return;
}
再入可能シナリオとアップグレード シナリオの両方で、_recursions が操作されます。_recursions は、ObjectMonitor に入る回数を記録し、ロック解除を完了するには、対応する数の終了操作を実行する必要があります。
アダプティブスピン
上記はすべてロックの取得に成功したシナリオですが、競合の結果失敗したシナリオはどうなるでしょうか? ObjectMonitor の最後から 2 番目の「軽量化」の追求である、アダプティブ スピンの部分を見てみましょう。
// 尝试自旋来竞争锁
Self->_Stalled = intptr_t(this);
if (Knob_SpinEarly && TrySpin (Self) > 0) {
Self->_Stalled = 0;
return;
}
objectMonitor#TrySpinメソッドは、アダプティブ スピニングのサポートです。Java 1.6 以降に追加され、デフォルトのスピン数が削除され、スピン数の決定は JVM に与えられます。
JVM はロックの最後のスピンに基づいて決定します。スピンが成功し、ロックを保持しているスレッドが実行中の場合、JVM は別のスピンの試行を許可します。ロックのスピンが頻繁に失敗する場合、JVM はスピン プロセスを直接スキップします。
ヒント:
- 適応スピンの元のコード分析は、重量ロックのソース コード分析に配置されます。
- objectMonitor#TryLockは非常にシンプルで、主要なテクノロジは依然として CAS です。
相互排除の実装
これまでのところ、CAS とスピンはどちらもバイアス ロックや軽量ロックで登場したテクノロジーですが、ObjectMonitor が「重量級」という評判を得ているのはなぜでしょうか?
そして最後に、競争が失敗するシナリオ:
// 此处省略了修改当前线程状态的代码
for (;;) {
EnterI(THREAD);
}
実際、ObjectMonitor#EnterIに入ると、最初に「軽量」ロック メソッドも試行されます。
void ObjectMonitor::EnterI(TRAPS) {
if (TryLock (Self) > 0) {
return;
}
if (TrySpin (Self) > 0) {
return;
}
}
次はヘビーウェイトの実際の実装です。
// 将当前线程(Self)封装为ObjectWaiter的node
ObjectWaiter node(Self);
Self->_ParkEvent->reset();
node._prev = (ObjectWaiter *) 0xBAD;
node.TState = ObjectWaiter::TS_CXQ;
// 将node插入到cxq的头部
ObjectWaiter * nxt;
for (;;) {
node._next = nxt = _cxq;
if (Atomic::cmpxchg(&node, &_cxq, nxt) == nxt)
break;
// 为了减少插入到cxq头部的次数,试试能否直接获取到锁
if (TryLock (Self) > 0) {
return;
}
}
ロジックは一目瞭然で、ObjectWaiterオブジェクトをカプセル化し、cxq キューの先頭に追加します。次に、下に移動して実行します。
// 将当前线程(Self)设置为当前ObjectMonitor的维护线程(_Responsible)
// SyncFlags的默认值为0,可以通过-XX:SyncFlags设置
if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
Atomic::replace_if_null(Self, &_Responsible);
}
for (;;) {
// 尝试设置_Responsible
if ((SyncFlags & 2) && _Responsible == NULL) {
Atomic::replace_if_null(Self, &_Responsible);
}
// park当前线程
if (_Responsible == Self || (SyncFlags & 1)) {
Self->_ParkEvent->park((jlong) recheckInterval);
// 简单的退避算法,recheckInterval从1ms开始
recheckInterval *= 8;
if (recheckInterval > MAX_RECHECK_INTERVAL) {
recheckInterval = MAX_RECHECK_INTERVAL;
}
} else {
Self->_ParkEvent->park();
}
// 尝试获取锁
if (TryLock(Self) > 0)
break;
if ((Knob_SpinAfterFutile & 1) && TrySpin(Self) > 0)
break;
if (_succ == Self)
_succ = NULL;
}
ロジックは複雑ではなく、現在のスレッドは常にパークされており、ウェイクアップされた後にロックの取得を試みます。-XX:SyncFlag
s の設定に注意する必要があります。
- その時点で
SyncFlags == 0
、同期はスレッドを直接一時停止しました。 - その時点で
SyncFlags == 1
、synchronized は指定された時間スレッドを一時停止します。
前者は永続的にサスペンドされているため、他のスレッドによって起動する必要がありますが、後者は指定された時間が経過すると自動的に起動されます。
ヒント:スレッドについて知っておくべき 8 つの質問 (中) park と parkEvent について説明しましたが、最下層は pthread_cond_wait と pthread_cond_timedwait によって実現されます。
重量ロックを解除する
重量ロックを解除するためのソース コードとコメントは非常に長いため、そのほとんどを省略し、重要な部分のみを説明します。
リエントリーロック出口
リエントリーでは _recursions の数が継続的に増加することがわかっているため、リエントリーを終了するシナリオは非常に単純です。
void ObjectMonitor::exit(bool not_suspended, TRAPS) {
Thread * const Self = THREAD;
// 第二次持有锁时,_recursions == 1
// 重入场景只需要退出重入即可
if (_recursions != 0) {
_recursions--;
return;
}
.....
}
_recursions のカウントを継続的にデクリメントします。
解放して書く
JVM の実装では、現在のスレッドがロック保持者であり、再度入っていない場合、最初に保持しているロックを解放し、次に変更をメモリに書き込み、最後に次のスレッドを起動する責任を負います。。まず、メモリの解放と書き込みのロジックを見てみましょう。
// 置空锁的持有者
OrderAccess::release_store(&_owner, (void*)NULL);
// storeload屏障,
OrderAccess::storeload();
// 没有竞争线程则直接退出
if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
TEVENT(Inflated exit - simple egress);
return;
}
次のステートメントの storeload バリア:
store1;
storeLoad;
load2
store1 命令の書き込みは、load2 命令の実行前にすべてのプロセッサに表示されることが保証されます。
ヒント: メモリ バリアについては、volatile で詳しく説明されています。
覚醒戦略
ロックを解放してメモリに書き込んだ後は、次のスレッドを起動してロックを使用する権利を「引き渡す」だけで済みます。しかし、「待機キュー」が cxq と EntryList の 2 つあり、どちらから起動すればよいでしょうか?
Java 11 より前は、QMode に応じてさまざまな戦略が選択されていました。
QMode == 0
デフォルトの戦略では、cxq を EntryList に入れます。QMode == 1
、cxqを反転し、EntryListに入れます。QMode == 2
、cxq から直接起動します。QMode == 3
、 cxq を EntryList の最後に移動します。QMode == 4
、 cxq を EntryList の先頭に移動します。
戦略が異なればウェイクアップ シーケンスも異なります。同期が不公平なロックである理由がわかりましたね。
objectMonitor#ExitEpilogメソッドは非常に単純で、パークに対応する unpark メソッドを呼び出すため、ここでは多くを説明しません。
ヒント: Java 12 の objectMonitor はQMode を削除します。これは、ウェイクアップ戦略が 1 つだけであることを意味します。
要約する
重量ロックについてまとめてみましょう。synchronized の強力なロックは ObjectMonitor で、使用される主要なテクノロジはCAS と parkです。mutex#Monitorと比較すると、本質は同じでパークをカプセル化していますが、ObjectMonitor は多くの最適化が行われた複雑な実装です。
重量ロックがどのようにリエントラントを実現するのか、そしてウェイクアップ戦略によって引き起こされる「不公平性」を見ていきました。次に、同期によって原子性、順序性、可視性が保証されるとよく言われますが、それはどのように実現されるのでしょうか?