しかし、マルチスレッドはどのように機能しますか? パートI:同期

注目を集める画像 (「ホットスポットのソースコードを複製しました。一緒に見てみましょう」シリーズからの投稿)

マルチスレッドの問題(パフォーマンスまたはあいまいなハイゼンバッグ)に直面しているすべての人は、それらを解決する過程で、必然的にインフレーション、競合、メンバー、バイアスロック、スレッドパーキングなどの用語に遭遇します。 しかし、誰もがこれらの用語の背後に隠されているものを本当に知っていますか? 残念ながら、実践が示すように、 すべてではありません



状況を修正することを期待して、私はこのトピックに関する一連の記事を書くことにしました。 それらはそれぞれ、 「最初に理論的に何が起こるべきかを簡単に説明し、次にソースに行き、そこでどうなるかを見る」という原則に基づいて構築されます。 したがって、最初の部分はJavaだけでなく、主に適用可能であるため、他のプラットフォームの開発者は自分にとって有益なものを見つけることができます。



詳細な説明を読む前に、Javaメモリモデルに精通していることを確認しておくと役立ちます。 たとえば、Sergey Walrus Kuksenkoのスライドや、私の初期のトピックで学習できます 。 また、スライド#38から始まるこのプレゼンテーションは素晴らしい資料です。



理論的最小値

ご存じのように、javaの各オブジェクトには独自のmonitorがあるため、同じC ++とは異なり、個別のmutex-sを使用してオブジェクトへのアクセスを保護する必要はありません。 フローの相互排除と同期の効果を実現するには、次の操作が使用されます。



続行する前に、重要な概念を定義します。



競合 -複数のエンティティが同時に同じリソースを所有しようとする状況。排他的使用を目的としています





モニターの所持に競合があるという事実は、モニターのキャプチャ方法に大きく依存します。 モニターの状態は次のとおりです。







この抽象的な推論で終わり、ホットスポットでの実装方法に没頭しています。



オブジェクトヘッダー

仮想マシン内のオブジェクトヘッダーには、通常、 マークワードとオブジェクトクラスへのポインターという 2つのフィールドが含まれます。 特別な場合には、たとえば配列の長さなど、そこに何かを追加できます。 これらのヘッダーは、いわゆるoop (通常のオブジェクトポインター)に格納され、ファイルhotspot/src/share/vm/oops/oop.hpp



でその構造を見ることができます。 同じフォルダーにあるmarkOop.hpp



ファイルに記述されているマークワードとは何かをより詳しく調べます。 oopDesc



からの継承には注意を払わないでください:これは歴史的な理由のみです)
良い方法で、詳細なコメントに注意を払って注意深く読む必要がありますが、あまり興味がない人のために、このマークの内容の簡単な説明を以下に示します単語が含まれているとどのような場合に。 このプレゼンテーションは、90番目のスライドから引き続き見ることができます。



単語の内容をマークする

状態 タグ付け 内容
ロックされていない、薄く、偏っていない 01
 Identity hashcode
      
      



 age
      
      



 0
      
      



ロックされ、薄く、偏りがない 00
     mark word
      
      



膨らんだ 10
    
      
      



偏った 01
 id -
      
      



 epoch
      
      



 age
      
      



 1
      
      



gcのマーク 11


ここにいくつかの新しい意味があります。 まず、 IDハッシュコードは、 System.identityHashCode



呼び出されたときに返されるオブジェクトのハッシュコードです。 第二に、 年齢はオブジェクトが生き残ったガベージコレクションの数です。 また、このオブジェクトのクラスのバルク失効またはバルクバイアスの数を示すエポックもあります。 これが必要な理由については、後で説明します。

バイアスの場合、アイデンティティハッシュコードとthreadID +エポックの両方に十分なスペースが同時にないことに気づきましたか? そして、これはそうであり、ここから興味深い結果があります:ホットスポットでは、 System.identityHashCode



を呼び出すと、オブジェクトの取り消しバイアスが発生します。

さらに、モニターがビジーの場合、マークワードは、実際のマークワードが保存されているマークワードに保存されます。 各スレッドのスタックには、さまざまなものが格納されるいくつかの「セクション」があります。 ロックレコードが保存されるものに興味があります。 そこで、オブジェクトのマークワードを軽量ロックでコピーします。 したがって、ちなみに、シンロックされたオブジェクトはスタックロックと呼ばれます 。 腫れたモニターは、膨張したストリームと、シックモニターのグローバルプールの両方に保存できます。



それでは、コードに行きましょう。



synchronized



を使用する簡単な例

このクラスから始めましょう:
 1 2 3 4 5 6 7
      
      



 public class SynchronizedSample { void doSomething() { synchronized(this) { // Do something } } }
      
      





それが何にコンパイルされるかを見てください:



 javac SynchronizedSample.java && javap -c SynchronizedSample
      
      





完全なリストは提供しませんが、 doSomething



メソッドの本体を使用して、コメントを提供します。



 void doSomething(); Code: 0: aload_0 //    this 1: dup //    (this) 2: astore_1 //      (this)   1 3: monitorenter //   ,     (this) 4: aload_1 //   this   5: monitorexit //   6: goto 14 //  "catch-" 9: astore_2 // (  ,   )     2 10: aload_1 //  this    11: monitorexit //   12: aload_2 //      13: athrow //   14: return //  Exception table: from to target type 4 6 9 any //      ,   "catch-" 9 12 9 any //   ,    
      
      







ここでは、 monitorenter



およびmonitorexit



興味がありmonitorenter



。 もちろん、あなたが選んだYandex検索エンジンで彼らがしていることを検索することはできますが、これは誤った情報に満ちており、どういうわけか悪い方法ではありません。 さらに、OpenJDKのソースが手元にあるので、 一般的に楽しむことができます 。 これらのソースでは、解釈モードのバイトコードで何が起こるかを見るのは非常に簡単です。 警告が1つだけあります。LeshaTheShade Shipilev 次のように述べています。

一般に、一部のアクションのVMヘルパーのコードは、JITによって貼り付けられたものと内容が異なる場合があります。 JITの一部の最適化がインタープリターに移植されない可能性がある点まで



また、リョーシャはPrintAssemblyを自分の歯に入れて、コンパイル済みのjitコード化されたコードを確認することを推奨しましたが、抵抗が少ないパスから始めて、それが本当にtmであるかを確認することにしました



モニター



インタプリタのソースはhotspot/src/share/vm/interpreter



にあり、それらの多くがあります。 この段階ですべてを読み直すことはあまりお勧めできません。grepを使用すると、必要な場所が見つかる可能性があるためです。 まず、 bytecodes.hpp



bytecodes.cpp



発表を見る価値があります。

 ./bytecodes.hpp:235: _monitorenter = 194, // 0xc2 ./bytecodes.cpp:489: def(_monitorenter, "monitorenter", "b", NULL, T_VOID, -1, true);
      
      





簡単に推測できるように、バイトコード.hpp



人間の列挙定数は.hpp



で定義され、この操作はdefメソッドを使用して.cpp



登録されます。 この記事のフレームワークでそれについて個別に話す意味はありません:monitorenterコマンドがmonitorenter



ていることを明確にするだけで十分です。これは、パラメーター( b



)のない単一のバイトコードであり、何も返さず、スタックから1つの値を引き出し、ロックを呼び出したり、セーフポイントを呼び出したりすることができます(後者については後で)。



以下は、 bytecodeInterpreter.cpp



ファイルに関係します。 すばらしいBytecodeInterpreter::run(interpreterState istate)



メソッドBytecodeInterpreter::run(interpreterState istate)



、これは約2200行しか必要とせず、通常、処理されたメソッドの本体が終了するまでループで回転します。 (実際、別の大きな部分は、メソッドの初期化、メソッドがsynchronized



場合のロックなど、他の有用なものを扱います)。 最後に、行1667



から、 monitorenter



操作が発生したときに何が起こるかをmonitorenter



ます。 まず、ストリームスタックに無料のモニターがあり(ない場合はistate->set_msg(more_monitors)



を使用してインタープリターから要求されistate->set_msg(more_monitors)



、ロック解除されたマークワードのコピーがそこに配置されます。 その後、CASを使用して、オブジェクトのマークワードにこのコピーへのポインターを書き込もうとします。これは、 displaced headerと呼ばれます。

CAS-比較と交換- *dest



compare_value



原子的に比較し、等しい場合は*dest



exchange_value



場所で比較exchange_value



ます。 初期値*dest



返されます。 (同時に、両面membarは保証されますが、次の記事でそれらについて詳しく説明します)



CASが成功した場合、勝利(およびそれによってモニター)は私たちのものであり、そこで終了できます(タグは、置き換えられたヘッダーへのポインター自体に含まれています-最適化)。 そうでない場合は、次に進みますが、最初に重要な点に注意を払います。 このモニターにバイアスがかかっているかどうかを確認しませんでした 。 Lyoshinの警告を思い出して、通訳には届かない最適化に出会ったことがわかります。 ところで、 synchronized



メソッドを処理するとき、すべてが正常にチェックされますが、それは少し後です。



CASが失敗した場合、モニターの所有者であるかどうかを確認します(再帰キャプチャ)。 もしそうなら、成功は再び私たちのものであり、私たちがすることはスタックのNULL



置き換えられたヘッダーに書き込むことだけです(後でそれが必要な理由がわかります)。 それ以外の場合は、次の呼び出しを行います。



 CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
      
      







マクロCALL_VM



は、フレームの作成など、あらゆる種類の技術的な操作を実行し、転送された機能を実行します。 私たちの場合、この関数はInterpreterRuntime::monitorenter



であり、これは新しいinterpreterRuntime.cpp



ファイルにあります。 monitorenter



メソッドは、 UseBiasedLocking



UseBiasedLocking



れているかどうかに応じて、 ObjectSynchronizer::fast_enter



またはObjectSynchronizer::slow_enter



ます。 最初のものから始めましょう。



fast_enter



まず、そのような最適化の適切性に関するいくつかの言葉。 IBM Research Labsの一部の東京の科学者は、未知の方法で統計を計算し、実際、ほとんどの場合、同期は競合しないことを発見しました。 さらに、さらに強力な声明が提案されました。ほとんどのオブジェクトのモニターは、生涯を通じて1つのストリームのみでキャプチャされます。 そして、偏りのあるロックのアイデアが生まれました。最初に起きた人、それ、そしてスリッパです。 バイアスロックの詳細については、たとえばこれらのスライドで読むことができます(ただし、多少古くなっています)。ホットスポットのソースに戻ります。 ここで、ファイルsrc/share/vm/runtime/synchronizer.cpp



に興味があります。169行目から始めsrc/share/vm/runtime/synchronizer.cpp



。最初に、自分自身にバイアスをかけ、うまくいかない場合は、取り消しを行い、通常のthin slow_enterに移動します。 楽観的な試みは、 biasedLocking.cpp



ファイルにあるBiasedLocking::revoke_and_rebias



で発生します。 より詳細に説明しましょう。





私は知っていたが、 tmを忘れてしまった人々に思い出させます。

safepoint-スレッドの実行が安全な場所で停止している仮想マシンの状態。 これにより、スレッドが現在所有しているモニターのバイアスの取り消し、最適化解除、スレッドダンプの取得など、侵入的な操作が可能になります。





この場合、 attempt_rebias



パラメーターは常にtrue



、たとえばVMスレッドからの呼び出しの場合など、 false



になることがあります。



ご想像のとおり、バルク操作は、スレッド間で多数のオブジェクトを簡単に転送できるようにするためのトリッキーな最適化です。 この最適化がなかった場合、デフォルトでUseBiasedLocking



を有効にするのは危険です。なぜなら、アプリケーションの大きなクラスは失効と再バイアスを永遠に処理するからです。



ストリームをすばやくキャプチャできない(つまり、取り消しバイアスが行われた)場合は、シンロックのキャプチャに進みます。



slow_enter



ここで興味のあるメソッドは、 src/share/vm/runtime/synchronizer.cpp



ファイルにあります。 ここにいくつかのシナリオがあります。



  1. ケース1、 「良い」 :オブジェクトのモニターは現在無料です。 次に、CASを使用して占有しようとします。 CASは既にアーキテクチャに依存する(通常はプロセッサによってネイティブにサポートされる)命令であるため、さらに深くする意味はありません。 (Ruslan#は彼にケレミンヘンウィートを与えましたが 、私は「MOSトランジスタのソースシンク内の電子の流れに到達しなかった」とinしました CASが成功すると、勝利:モニターがキャプチャされます。 それが失敗すると、悲しみに満たされ、3番目のケースに進みます。



  2. ケース2も「良好」です。オブジェクトのモニターは無料ではありませんが、現在キャプチャしようとしている同じストリームでビジーです。 これは再帰的なキャプチャです。 この場合、簡単にするために、スタックNULLのディスプレイスされたヘッダーに書き込みます。これは、以前にこのモニターを既にキャプチャし、スタックに関する情報をすでに持っているためです。



  3. ケース3、 「悪い」膨張 :だから、失敗したトリックを見せようとする試みはすべて失敗しました。そして、下品なユーザー言語を見せて、セグメンテーション違反に陥る以外にやることはありません。 ハハ 冗談。 ただし、ふくれっ面については何も言いませんでした。OSレベルのプリミティブに頼って「正直に」行動しなければならない場合、 モニターが膨らんだと言います。 コードでは、この動作はObjectSynchronizer::inflate



    で説明されていますが、ここではあまり注意しません。実際、このメソッドはスレッドセーフであり、いくつかの技術的な微妙な点を考慮して、フラグをモニターに設定します。



    モニターを膨らませた後、モニターに入る必要があります。 ObjectMonitor::enter



    メソッドはまさにそれを行い、考えられない考えられないすべてのトリックを適用して、ストリームのObjectMonitor::enter



    を回避します。 これらのトリックには、ご想像のとおり、1回限りのCAS-sやその他の「フリーメソッド」を使用して、スピンループを使用してキャプチャを試みることが含まれます。 ちなみに、コメントと何が起こっているのかとの間にわずかな矛盾が見つかったようです。 一度スピンループでモニターに入ろうとすると、一度だけ行うと主張します。



     352 353 354 355 356 357 358 359 360 361 362 363
          
          



     // Try one round of spinning *before* enqueueing Self // and before going through the awkward and expensive state // transitions. The following spin is strictly optional ... // Note that if we acquire the monitor from an initial spin // we forgo posting JVMTI events and firing DTRACE probes. if (Knob_SpinEarly && TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_recursions == 0 , "invariant") ; assert (((oop)(object()))->mark() == markOopDesc::encode(this), "invariant") ; Self->_Stalled = 0 ; return ; }
          
          







    そして、ここでもう少し、呼び出されたenterI



    メソッドで、もう一度実行します。



     485 486 487 488 489 490 491 492 493 494 495 496 497
          
          



     // We try one round of spinning *before* enqueueing Self. // // If the _owner is ready but OFFPROC we could use a YieldTo() // operation to donate the remainder of this thread's quantum // to the owner. This has subtle but beneficial affinity // effects. if (TrySpin (Self) > 0) { assert (_owner == Self , "invariant") ; assert (_succ != Self , "invariant") ; assert (_Responsible != Self , "invariant") ; return ; }
          
          







    さて、オペレーティングシステムレベルでの駐車は非常に怖いので、ほとんど何でもそれを回避する準備ができています。 彼女の何がひどいのか見てみましょう。



    駐車ストリーム

    かなり前に書いたコードに近づいていると言わなければなりませんが、これは顕著です。 多くの複製、リエンジニアリング、その他の設備があります。 ただし、「この松葉杖を取り除いて」や「これらを組み合わせて」などのコメントの存在は少し安心です。



    それでは、駐車ストリームとは正確には何ですか? 各モニターにはいわゆるEntry ListWaitsetと混同しないでください )があることを誰もが聞いたことがあるはずです。 モニターへの低価格での入力試行がすべて失敗した後、この特定のキューに自分自身を追加し、その後、駐車します。



     591 592 593 594 595 596 597 598 599 600 601
          
          



     // park self if (_Responsible == Self || (SyncFlags & 1)) { TEVENT (Inflated enter - park TIMED) ; Self->_ParkEvent->park ((jlong) RecheckInterval) ; // Increase the RecheckInterval, but clamp the value. RecheckInterval *= 8 ; if (RecheckInterval > 1000) RecheckInterval = 1000 ; } else { TEVENT (Inflated enter - park UNTIMED) ; Self->_ParkEvent->park() ; }
          
          







    駐車場に直接移動する前に、現在のストリームに責任があるかどうかに応じて、ここで時間を調整できるかどうかを確認します。 責任のあるフローは常に1つだけであり、いわゆるストランディングを回避するために必要です。モニターが空いているときの悲しみですが、待機セット内のすべてのフローはまだ待機しており、奇跡を待っています。 担当者がいるとき、彼は時々自動的に目を覚まします(より多くの無駄な目覚めが起きた-目覚めた後、ロックを取得できませんでした-駐車時間は長くなります。1000msを超えないことに注意してください)。 残りのフローは、少なくとも永遠に目覚めを待つことができます。



    それでは、駐車の本質に移りましょう。 すでに理解しているように、これはにwait/notify



    wait/notify



    Java開発者に似たセマンティクス上のものですが、オペレーティングシステムレベルで発生します。 たとえば、Linuxおよびbsdでは、ご想像のとおり、POSIXスレッドが使用されます。POSIXスレッドでは、 pthread_cond_timedwait



    (またはpthread_cond_wait



    )が呼び出され、モニターが解放されるのを待ちます。 これらのメソッドは、LinuxスレッドのステータスをWAITINGに変更し、何らかのイベントが発生したときにシステムスケジューラにウェイクアップするように要求します(ただし、このスレッドが責任を負うのは一定期間後までです)。



    OSレベルのスレッドスケジューリング

    それでは、Linuxカーネルを調べて、そこでスケジューラーがどのように機能するかを見てみましょう。 ご存知のように、Linuxソースはgitにあり、次のようにシェダーをクローンできます。



     git clone git://git.kernel.org/pub/scm/linux/kernel/git/rostedt/linux-rt.git
          
          







    フォルダに行きましょう...わかりました、わかりました、冗談です。 ソースコードの行に対して正確なLinuxシェダーデバイスの機能は、単純なsynchronized



    ブロックから始めた無害な記事には既に深すぎます。ハードコアを減らすために、シェダーが一般的にどのように機能するかを説明します。 linux, . - , , : — , . — quantum — , , , . linux , 10-200 , 1 . windows , 2-15 , — 10 15 .



    , , , . , , (, - I/O-), , - . - . , , , -, , .



    , , , — . , , , , , , .



    , , : , — , . : , , -, .



    , , , . , contention, .




monitorexit



バイアスロックの場合、基本的には何もしません。置き換えられたヘッダーにはNULLが格納されていることがわかり、終了します。これは興味深い点です。現在占有されていないバイアスロックを解除しようとすると、インタープリターはスローされませんIllegalMonitorStateException



(ただし、バイトコード検証はそのようなことを監視します)。



偏りのないロックの場合、を呼び出しますInterpreterRuntime::monitorexit



。いくつかのチェックの後(たとえば、モニターが実際にロックされているかどうか、そうでない場合はスローされますIllegalMonitorStateException



)、呼び出されObjectSynchronizer::slow_exit



ますfast_exit



ソースを読む場合、高速パスに関するコメントに注意を払ってはいけません。この方法では、次のシナリオが可能です。モニターがスタックロック状態になっている膨張または膨張。最初のケースでは、すべてが単純です。オブジェクトヘッダーをロック前の状態に戻し、終了します。 2番目のケースでは、誰かがモニターの膨らみを終えて3番目のケースに進むのを待ちます。



3番目のケースでは、ロックを解除し、膜を露出します。その後、現在ロックを取得する準備ができている駐車ストリームがあるかどうかを確認します。これは、彼が目を覚まして、たとえばTrySpin



(上記を参照)を使用してモニターをキャプチャしようとする場合に可能です。これが見つかった場合、これに関する作業は完了します。ロックを受け取りたいスレッドのキューが空の場合も完了します。



そのようなフローがない場合は、ポリシーに応じて(を使用して設定Knob_QMode



正直なところ、その値が0



デフォルト設定から変化する単一の場所を見つけていませんただし、知識のある人tmは、これがデバッグとチューニングの残りである可能性が高いことを示唆しています)、最初に目を覚ます人を選択します。これらは最近目覚めたストリームかもしれませんし、逆に最近目覚めたストリームかもしれません。短い一連の呼び出しの後、プラットフォーム固有のコードを見つけますos::PlatformEvent::unpark()



。このコードは、目的のストリームに信号を伝えます。たとえば、linuxとbsdが使用されpthread_cond_signal



ます。



実際、あまり詳しく説明しなければ、モニターのリリースについて言えることはこれだけです。



NB:synchronized



メソッド



次のように元のJavaコードを記述した場合:

 synchronized void doSomething() { // Do something }
      
      





このメソッドのバイトコードは著しく短くなります:



 synchronized void doSomething(); Code: 0: return
      
      







インbytecodeInterpreter.cpp



synchronized



メソッドは、行から処理されます767



チェックがありif (METHOD->is_synchronized())



ます。この条件の内部には、if



偏りのあるロックに関連する膨大な数のがあります。これは、操作を処理するときに突然表示されなかったものmonitorenter



です。一般に、前に説明したことが行われていますが、所有者スレッドによるバイアスのかかったモニターの(CASなしの)迅速なキャプチャがまだあります。



また、メソッド本体の実行が終了した後、メソッドが同期されるとモニターは終了します。



待って通知する



これらのメソッドの処理は、synchronizer.cpp



377行目から始まります。インタプリタで待機/通知しているモニターは、これらのメソッドが最初に行うことで膨らませるので、膨らませる必要があります。その後、彼らは彼のメソッドwait



またはを呼び出しますnotify







最初の1つは、待機セット(実際にはターン)に追加され、ウェイクアップする時間になるまで待機します(待機するように要求された時間が経過しました。中断または通知と呼ばれる人がいました)。



Notify



待機セットから1つのストリームをプルし、ポリシーに応じて、モニターをキャプチャしたい人のキューのある場所に追加します。NotifyAll



全員が待機セットから抜け出すという点でのみ異なります。



メモリ効果



JMMに精通している人は、同じモニターをキャプチャする前にモニターのリリースが行われることを知っています。シンの場合、これはCASによって保証されます。膨らんだ場合、これは明示的な呼び出しによって保証されますOrderAccess::fence();



モニターが偏っている場合、それは1つのスレッドのみがモニターを使用することを意味します。その実行はすでにプログラムの順序で保証されています。HBを取り消すと、monitorexitの最中(ストリームが生きていた場合)(すでに薄いか膨張している)、または入力したとき(これも薄いか膨張している)が表示されます。



HBを保証するために、待機を終了する直前に明示的なフェンスが設定されます。



Major O. aka Disclaimer からのコメント

実際、tm、すべては私たちが考えているようには起こりません。たとえば、JITがコードをネイティブにコンパイルする場合。または、他の仮想マシンで作業する場合。ただし、「ホットスポットのインタープリター」という単純なケースで、すべてが実際に私がここで書いた方法であることを保証することはできません。



お楽しみに

次のシリーズでは、まず、メモリーバリアについて説明する必要があります。これは、JMMで発生前を提供するために非常に重要ですこれらはvolatile



、今後行うフィールドの例について検討するのに非常に便利です。最終フィールドと安全な出版物にも注意を払う価値がありますが、TheShadechereminはすでにそれらの 記事それらカバーいるので、読書に興味のある人にそれらを読むことができます(慎重にのみ)。そして最後に、JITが入ってきたときの違いについてPrintAssemblyでいっぱいのストーリーを待つことができます



そしてもう一つ©

旅行を繰り返したい人のために:jdk7uの144f8a1a43cbリビジョンを使用しました。リビジョンが異なる場合、行番号も異なる場合があります-K.O.



バイアスロックは、仮想マシンの起動直後ではなく、BiasedLockingStartupDelay



ミリ秒(デフォルトでは4000)後にオンになります。これは、そうでない場合、仮想マシンの起動と初期化、クラスのロード、その他すべてのプロセスで、生きているオブジェクトの一定の取り消しバイアスが原因で膨大な数のセーフポイントが表示されるためです。



すべてのセーフポイントで、メソッドはObjectSynchronizer::deflate_idle_monitors



何をしているかを非常に簡単に理解できる名前で呼び出されます。



勇敢なTheShadeartyushovchereminAlexeyTokar感謝ます 彼らが(あなたについて)出版前に記事を読み、愚かな冗談やandに満ちた光の代わりに、ナンセンスを大衆に持ち込まないことを確かめた。



All Articles