25

私は、メモリにマップされたファイル ( FileChannel.map()を介して) から構築されたByteBuffersと、メモリ内の直接 ByteBuffers を使用するものに取り組んでいます。並行性とメモリ モデルの制約を理解しようとしています。

FileChannel、ByteBuffer、MappedByteBuffer などの関連するすべての Javadoc (およびソース) を読みました。特定の ByteBuffer (および関連するサブクラス) に多数のフィールドがあり、状態がメモリ モデルから保護されていないことは明らかです。視点。そのため、特定の ByteBuffer の状態を変更するときに、そのバッファーが複数のスレッドで使用されている場合は同期する必要があります。一般的なトリックには、ThreadLocal を使用して ByteBuffer をラップする、(同期中に) 複製して同じマップされたバイトを指す新しいインスタンスを取得するなどがあります。

このシナリオを考えると:

  1. B_allマネージャーには、ファイル全体にマップされたバイト バッファーがあります (2 GB 未満とします)。
  2. マネージャーは、B_all で duplicate()、position()、limit()、および slice() を呼び出して、ファイルのチャンクである新しいより小さい ByteBuffer を作成し、B_1これをスレッド T1 に渡します。
  3. B_2マネージャーは、同じマップされたバイトを指すByteBuffer を作成するためにすべて同じことを行い、これをスレッド T2 に渡します

私の質問は、T1 が B_1 に書き込み、T2 が B_2 に同時に書き込み、互いの変更を確実に確認できるかどうかです。T3 は B_all を使用してこれらのバイトを読み取り、T1 と T2 の両方からの変更を確実に確認できますか?

force() を使用して OS にページをディスクに書き込むように指示しない限り、マップされたファイルへの書き込みが必ずしもプロセス間で見られるとは限らないことを認識しています。私はそれを気にしません。この質問では、この JVM が単一のマップされたファイルを書き込む唯一のプロセスであると仮定します。

注: 私は推測を求めているわけではありません (自分で推測することはできます)。メモリ マップド ダイレクト バッファで何が保証されているか (または保証されていないか) について、決定的なものへの参照を希望します。または、実際の経験や否定的なテスト ケースがあれば、それも十分な証拠となる可能性があります。

更新:複数のスレッドが同じファイルに並行して書き込むことでいくつかのテストを行ったところ、これまでのところ、それらの書き込みは他のスレッドからすぐに見えるようです。そこまで頼れるか不安ですけどね。

4

7 に答える 7

16

JVM によるメモリ マッピングは、CreateFileMapping (Windows) または mmap (posix) のシン ラッパーにすぎません。そのため、OS のバッファ キャッシュに直接アクセスできます。これは、これらのバッファーが、OS がファイルに含まれていると見なすものであることを意味します (そして、OS は最終的にこれを反映するためにファイルを同期します)。

したがって、プロセス間で同期するために force() を呼び出す必要はありません。プロセスはすでに同期されています (OS を介して - 読み取り/書き込みでも同じページにアクセスします)。OS とドライブ コントローラー間の同期のみを強制します (ドライブ コントローラーと物理プラッターの間に遅延が発生する可能性がありますが、それについて何もするためのハードウェア サポートはありません)。

いずれにせよ、メモリ マップト ファイルは、スレッドやプロセス間の共有メモリとして認められた形式です。この共有メモリと、たとえば、Windows の仮想メモリの名前付きブロックとの唯一の違いは、ディスクへの最終的な同期です (実際、mmap は、/dev/null をマッピングすることにより、ファイルを使わずに仮想メモリを実行します)。

プロセッサは順不同で実行できるため (これが JVM とどの程度相互作用するかは不明ですが、推測はできません)、複数のプロセス/スレッドからのメモリの読み取りと書き込みには、まだある程度の同期が必要ですが、そこからバイトを書き込みます。 1 つのスレッドは、ヒープ内の任意のバイトへの通常の書き込みと同じ保証を持ちます。書き込みが完了すると、すべてのスレッドとすべてのプロセスが更新を認識します (open/read 操作を介しても)。

詳細については、posix で mmap を調べてください (または Windows の場合は CreateFileMapping で、これはほぼ同じ方法で作成されています。

于 2011-08-11T04:11:50.910 に答える
5

いいえ。JVM メモリ モデル (JMM) は、(非同期の) データを変更する複数のスレッドが互いの変更を認識することを保証しません。

まず、共有メモリにアクセスするすべてのスレッドがすべて同じ JVM 内にあるとすると、このメモリがマップされた ByteBuffer を介してアクセスされているという事実は無関係です (ByteBuffer を介してアクセスされるメモリには、暗黙的な揮発性または同期はありません)。バイト配列へのアクセスに関するものと同等です。

質問を言い換えて、バイト配列に関するものにしましょう。

  1. マネージャーにはバイト配列があります。byte[] B_all
  2. その配列への新しい参照が作成されbyte[] B_1 = B_all、スレッドに渡されます:T1
  3. その配列への別の参照が作成されbyte[] B_2 = B_all、スレッドに渡されます:T2

スレッドごとの書き込みはB_1スレッドごとT1に表示さB_2れますT2か?

T_1いいえ、そのような書き込みは、 と の間で明示的な同期が行われない限り、確実に表示されるわけではありませんT_2。問題の核心は、JVM の JIT、プロセッサ、およびメモリ アーキテクチャが一部のメモリ アクセスの順序を自由に変更できることです (単に腹を立てるだけでなく、キャッシングによってパフォーマンスを向上させるためです)。これらのレイヤーはすべて、同期が必要な場所についてソフトウェアが (ロック、揮発性、またはその他の明示的なヒントを介して) 明示的であることを期待しており、そのようなヒントが提供されていない場合、これらのレイヤーは自由に物を移動できることを意味します。

実際には、書き込みが表示されるかどうかは、主にハードウェアと、キャッシュとレジスタのさまざまなレベルでのデータの配置、および実行中のスレッドがメモリ階層内でどれだけ離れているかに依存することに注意してください。

JSR-133 は、Java 5.0 頃の Java メモリ モデルを正確に定義するための取り組みでした (私の知る限り、2012 年にまだ適用可能です)。それは、決定的な(密集した)答えを探したい場所です:http ://www.cs.umd.edu/~pugh/java/memoryModel/jsr133.pdf (セクション2が最も関連性があります)。より読みやすいものは、JMM Web ページにあります: http://www.cs.umd.edu/~pugh/java/memoryModel/

私の答えの一部は、データ同期に関してByteBuffera と a に違いはないと断言することです。これを説明している特定のドキュメントは見つかりませんが、 java.nio.Bufferbyte[]ドキュメントの「スレッド セーフティ」セクションで、同期または揮発性が該当する場合はそれについて言及することをお勧めします。ドキュメントではこれについて言及されていないため、このような動作は期待できません。

于 2012-04-19T07:51:12.560 に答える
3

最も安価な方法は、volatile 変数を使用することです。スレッドは、マップされた領域に書き込みを行った後、揮発性変数に値を書き込む必要があります。読み取りスレッドは、マップされたバッファーを読み取る前に volatile 変数を読み取る必要があります。これを行うと、Java メモリ モデルで "happens-before" が生成されます。

別のプロセスが何か新しいものを書き込んでいるという保証はないことに注意してください。しかし、他のスレッドがあなたが書いたものを見ることができることを保証したい場合は、volatile を書く (続いて読み取りスレッドから読み取る) ことでうまくいきます。

于 2012-02-17T05:32:16.473 に答える
1

ダイレクトメモリは、ヒープメモリと同じ保証またはそれらの欠如を提供すると思います。基になる配列またはダイレクト メモリ アドレスを共有する ByteBuffer を変更する場合、2 番目の ByteBuffer は別のスレッドで変更を確認できますが、そうする保証はありません。

同期または揮発性を使用しても、動作が保証されていないのではないかと思いますが、プラットフォームによっては動作する可能性があります。

スレッド間でデータを変更する簡単な方法は、エクスチェンジャーを使用することです

例に基づいて、

class FillAndEmpty {
   final Exchanger<ByteBuffer> exchanger = new Exchanger<ByteBuffer>();
   ByteBuffer initialEmptyBuffer = ... a made-up type
   ByteBuffer initialFullBuffer = ...

   class FillingLoop implements Runnable {
     public void run() {
       ByteBuffer currentBuffer = initialEmptyBuffer;
       try {
         while (currentBuffer != null) {
           addToBuffer(currentBuffer);
           if (currentBuffer.remaining() == 0)
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ... }
     }
   }

   class EmptyingLoop implements Runnable {
     public void run() {
       ByteBuffer currentBuffer = initialFullBuffer;
       try {
         while (currentBuffer != null) {
           takeFromBuffer(currentBuffer);
           if (currentBuffer.remaining() == 0)
             currentBuffer = exchanger.exchange(currentBuffer);
         }
       } catch (InterruptedException ex) { ... handle ...}
     }
   }

   void start() {
     new Thread(new FillingLoop()).start();
     new Thread(new EmptyingLoop()).start();
   }
 }
于 2011-08-09T20:37:45.880 に答える
1

私が遭遇した可能性のある答えの 1 つは、ファイル ロックを使用して、バッファーによってマップされたディスクの部分への排他的アクセスを取得することです。これは、たとえば、ここで例を挙げて説明されています。

これにより、ファイルの同じセクションへの同時書き込みを防ぐためにディスクセクションが実際に保護されると思います。ディスク ファイルのセクションに対する Java ベースのモニターを使用して、同じことを (単一の JVM 内で、他のプロセスからは見えないように) 実現できます。外部プロセスから見えないという欠点があるため、それはより高速になると思います。

もちろん、jvm/os によって一貫性が保証されている場合は、ファイルのロックやページの同期は避けたいと思います。

于 2011-08-10T15:03:25.157 に答える
0

いいえ、通常のJava変数や配列要素と同じです。

于 2011-08-09T21:53:58.923 に答える
0

これが保証されているとは思いません。Java メモリ モデルが保証されていると述べていない場合、それは定義上保証されていません。すべての書き込みを処理する 1 つのスレッドの同期書き込みまたはキュー書き込みでバッファー書き込みを保護します。後者は、マルチコア キャッシングでうまく機能します (RAM の場所ごとに 1 つのライターを使用することをお勧めします)。

于 2011-08-09T20:36:43.217 に答える