web-dev-qa-db-ja.com

Java)での同期のメモリ効果

JSR-133 FAQ 言います:

しかし、同期には相互排除以上のものがあります。同期により、同期されたブロックの前または最中のスレッドによるメモリ書き込みが、同じモニター上で同期する他のスレッドに予測可能な方法で表示されるようになります。同期ブロックを終了した後、モニターを解放します。これは、キャッシュをメインメモリにフラッシュする効果があるため、このスレッドによって行われた書き込みを他のスレッドに表示できます。同期ブロックに入る前に、モニターを取得します。これは、ローカルプロセッサのキャッシュを無効にして、変数がメインメモリから再ロードされるようにする効果があります。これで、前のリリースで表示されたすべての書き込みを確認できるようになります。

また、最近のSun VMでは、競合のない同期は安価であることを読んだことも覚えています。私はこの主張に少し混乱しています。次のようなコードを検討してください。

class Foo {
    int x = 1;
    int y = 1;
    ..
    synchronized (aLock) {
        x = x + 1;
    }
}

Xの更新には同期が必要ですが、ロックを取得すると、キャッシュからyの値もクリアされますか?それが本当なら、ロックストライピングのようなテクニックは役に立たないかもしれないので、私はそれが事実であるとは想像できません。あるいは、JVMはコードを確実に分析して、同じロックを使用する別の同期ブロックでyが変更されないようにし、同期ブロックに入るときにyの値をキャッシュにダンプしないようにすることができますか?

43
Binil Thomas

簡単に言えば、JSR-133の説明は行き過ぎです。 JSR-133は非規範的なドキュメントであり、言語またはJVM標準の一部ではないため、これは深刻な問題ではありません。むしろ、メモリモデルを実装するための十分である1つの可能な戦略を説明するドキュメントにすぎませんが、一般的には必要ではありません。その上、「キャッシュフラッシング」についてのコメントは基本的に完全に場違いです。なぜなら、本質的にゼロのアーキテクチャは、あらゆるタイプの「キャッシュフラッシング」を実行することによってJavaメモリモデルを実装するからです(そして多くのアーキテクチャはそのような指示さえありません)。

Javaメモリモデルは、可視性、アトミック性、発生前の関係などの観点から正式に定義されており、どのスレッドを正確に説明します必須何を、何を参照するかアクション必須正確に(数学的に)定義されたモデルを使用して、他のアクションや他の関係の前に発生します。正式に定義されていない動作はランダムであるか、一部のハードウェアおよびJVM実装で実際に明確に定義されている可能性があります-しかしもちろん、これは将来変更される可能性があるため、決して信頼すべきではありません。また、JVMを作成し、ハードウェアのセマンティクスを十分に理解していない限り、そもそもこれが明確に定義されていることを確信できません。

したがって、引用したテキストは、Javaが保証するものを正式に説明しているのではなく、メモリの順序付けと可視性が非常に弱い仮想アーキテクチャがどのように保証するかを説明していますcould Javaキャッシュフラッシュを使用したメモリモデルの要件。キャッシュフラッシュ、メインメモリなどの実際の説明は、明らかに一般的にJavaこれらの概念には当てはまらないため、適用されません。 tは抽象言語とメモリモデルの仕様に存在します。

実際には、メモリモデルによって提供される保証は、フルフラッシュよりもはるかに弱く、すべてのアトミック、同時実行関連、またはロック操作でキャッシュ全体をフラッシュするのは非常にコストがかかります。これは実際にはほとんど行われません。むしろ、特別なアトミックCPU操作が使用され、場合によっては メモリバリア 命令と組み合わせて使用​​されます。これにより、メモリの可視性と順序が保証されます。したがって、安価な競合のない同期と「キャッシュの完全なフラッシュ」の間の明らかな不一致は、最初の同期が真で2番目の同期が真ではないことに注意することで解決されます。Javaメモリモデル(Java $ ===)では完全なフラッシュは必要ありません。実際にはフラッシュは発生しません)。

正式なメモリモデルが少し重すぎて消化できない場合(あなたは一人ではないでしょう)、 Doug Leaのクックブック を見て、このトピックをさらに深く掘り下げることもできます。これは実際にリンクされていますJSR-133 FAQにありますが、コンパイラの作成者を対象としているため、具体的なハードウェアの観点から問題が発生します。そこでは、同期を含む特定の操作に必要な障壁について正確に説明しています。そこで説明されている障壁は、実際のハードウェアに非常に簡単にマッピングできます。実際のマッピングの多くは、クックブックで説明されています。

42
BeeOnRope

BeeOnRopeは正しいです。引用するテキストは、Javaメモリモデルが実際に保証するものよりも、一般的な実装の詳細を詳しく掘り下げています。実際には、yが実際にCPUキャッシュから削除されるのは次の場合です。 xで同期します(また、例のxが揮発性変数である場合、効果をトリガーするために明示的な同期は必要ありません)。これはほとんどのCPUで発生するためです(これはハードウェア効果であり、JMMが説明するものではないことに注意してください)。 )、キャッシュはキャッシュラインと呼ばれるユニットで機能します。キャッシュラインは通常、マシンワードよりも長くなります(たとえば、64バイト幅)。キャッシュにロードまたは無効化できるのは完全なラインのみであるため、xとyが落ちる可能性が高くなります。同じ行に入れて、それらの1つをフラッシュすると、もう1つもフラッシュされます。

この効果を示すベンチマークを書くことが可能です。揮発性のintフィールドが2つしかないクラスを作成し、2つのスレッドにいくつかの操作(長いループでのインクリメントなど)を実行させます。1つはフィールドの1つで、もう1つはもう1つです。操作の時間を計ります。次に、2つの元のフィールドの間に16個のintフィールドを挿入し、テストを繰り返します(16 * 4 = 64)。配列は単なる参照であるため、16個の要素の配列ではうまくいきません。一方のフィールドでの操作がもう一方のフィールドに影響を与えなくなるため、パフォーマンスが大幅に向上する場合があります。これが機能するかどうかは、JVMの実装とプロセッサアーキテクチャによって異なります。私はこれをSunJVMと典型的なx64ラップトップで実際に見ましたが、パフォーマンスの違いは数倍でした。

10

Xの更新には同期が必要ですが、ロックを取得すると、キャッシュからyの値もクリアされますか?それが本当なら、ロックストライピングのようなテクニックは役に立たないかもしれないので、私はそれが事実であるとは想像できません。

よくわかりませんが、答えは「はい」かもしれません。このことを考慮:

class Foo {
    int x = 1;
    int y = 1;
    ..
    void bar() {
        synchronized (aLock) {
            x = x + 1;
        }
        y = y + 1;
    }
}

プログラムの残りの部分で何が起こるかによっては、このコードは安全ではありません。ただし、メモリモデルとは、yから見たbarの値が、ロック取得時の「実際の」値よりも古くならないことを意味すると思います。これは、yxのキャッシュを無効にする必要があることを意味します。

また、JVMはコードを確実に分析して、同じロックを使用して別の同期ブロックでyが変更されていないことを確認できますか?

ロックがthisの場合、この分析は、すべてのクラスがプリロードされると、グローバル最適化として実行可能であるように見えます。 (私はそれが簡単である、または価値があると言っているのではありません...)

より一般的なケースでは、特定のロックが特定の「所有」インスタンスに関連してのみ使用されることを証明する問題は、おそらく手に負えないものです。

7
Stephen C

jdk6.0のドキュメントを確認することをお勧めします http://Java.Sun.com/javase/6/docs/api/Java/util/concurrent/package-summary.html#MemoryVisibility

メモリ整合性プロパティJava言語仕様の第17章では、共有変数の読み取りや書き込みなどのメモリ操作の発生前の関係を定義しています。1つのスレッドによる書き込みの結果が表示されることが保証されています。書き込み操作が発生した場合にのみ、別のスレッドによる読み取り(読み取り操作の前)。同期された揮発性コンストラクト、およびThread.start()メソッドとThread.join()メソッドは、発生前の関係を形成できます。特に:

  • スレッド内の各アクションは、プログラムの順序の後半にあるスレッド内のすべてのアクションの前に発生します。
  • モニターのロック解除(同期ブロックまたはメソッド出口)が発生します-同じモニターの後続のすべてのロック(同期ブロックまたはメソッド入力)の前に。また、発生前の関係は推移的であるため、ロック解除前のスレッドのすべてのアクションは、そのモニターをロックした後のすべてのアクションの前に発生します。
  • 揮発性フィールドへの書き込みは、同じフィールドの後続のすべての読み取りの前に発生します。揮発性フィールドの書き込みと読み取りには、モニターの出入りと同様のメモリ整合性効果がありますが、相互排他ロックは必要ありません。
  • スレッドで開始するための呼び出しが発生します-開始されたスレッドでのアクションの前に。
  • スレッド内のすべてのアクションが発生します-他のスレッドがそのスレッドの結合から正常に戻る前に

したがって、上記の強調表示されたポイントで述べたように、モニターでロック解除が発生する前に発生するすべての変更は、同じモニターでロックを取得するすべてのスレッド(および独自の同期ブロック内)に表示​​されます。これは、Javaの発生に準拠しています。 -セマンティクスの前。したがって、他のスレッドが「aLock」でモニターを取得すると、yに加えられたすべての変更もメインメモリにフラッシュされます。

4
rohit kochar

私たちはJava開発者です。私たちは仮想マシンしか知りません。実際のマシンは知りません!

何が起こっているのかを理論化させてください-しかし、私は何について話しているのかわからないと言わなければなりません。

たとえば、スレッドAはキャッシュAを備えたCPU Aで実行されており、スレッドBはキャッシュBを備えたCPUBで実行されています。

  1. スレッドAはyを読み取ります。 CPU Aはメインメモリからyをフェッチし、その値をキャッシュAに保存しました。

  2. スレッドBは新しい値を「y」に割り当てます。 VMこの時点でメインメモリを更新する必要はありません。スレッドBに関する限り、「y」のローカルイメージで読み取り/書き込みを行うことができます。おそらく「y」 'はCPUレジスタに他なりません。

  3. スレッドBは同期ブロックを終了し、モニターを解放します。 (いつどこでブロックに入ったかは関係ありません)。スレッドBは、この時点までに「y」を含むかなりの数の変数を更新しています。これらの更新はすべて、今すぐメインメモリに書き込む必要があります。

  4. CPU Bは、新しいy値を書き込んで「y」をメインメモリに配置します。 (私は想像します)ほぼ瞬時に、情報「メインyが更新されます」はキャッシュAに配線され、キャッシュAはyの自身のコピーを無効にします。それはハードウェア上で本当に速く起こったに違いありません。

  5. スレッドAはモニターを取得し、同期ブロックに入ります。この時点では、キャッシュAに関して何もする必要はありません。「y」はすでにキャッシュAから削除されています。スレッドAが再びyを読み取ると、メインメモリから新しいです。 Bによって割り当てられた新しい値。

別の変数zについて考えてみます。これも、ステップ(1)でAによってキャッシュされましたが、ステップ(2)でスレッドBによって更新されていません。ステップ(5)までキャッシュAで存続できます。 'z'へのアクセスは、同期のために遅くなることはありません。

上記のステートメントが理にかなっている場合、実際にコストはそれほど高くありません。


step(5)への追加:スレッドAは、キャッシュAよりもさらに高速な独自のキャッシュを持っている場合があります。たとえば、変数「y」のレジスタを使用できます。これはステップ(4)によって無効化されないため、ステップ(5)では、スレッドAは同期に入るときに自身のキャッシュを消去する必要があります。しかし、それは大きなペナルティではありません。

4
irreputable

同期は、1つのスレッドのみがコードのブロックに入ることができることを保証します。ただし、同期セクション内で行われた変数の変更が他のスレッドに表示されることを保証するものではありません。同期ブロックに入るスレッドのみが変更を確認できることが保証されています。 Javaでの同期のメモリ効果は、c ++に関するダブルチェックロックの問題と比較できます。Javaダブルチェックロックは広く引用され、使用されていますマルチスレッド環境でレイジー初期化を実装するための効率的な方法として。残念ながら、それはJavaで実装された場合、プラットフォームに依存しない方法で確実に機能しません、追加の同期なしで。 C++としては、プロセッサのメモリモデル、コンパイラによって実行される並べ替え、およびコンパイラと同期ライブラリ間の相互作用に依存します。これらはいずれもC++などの言語で指定されていないため、ほとんど何も言えません。明示的なメモリバリアを使用してC++で機能させることができますが、これらのバリアはJavaでは使用できません。

1