CodeGym/Java Course/モジュール 3/JVM のメモリ、パート 2

JVM のメモリ、パート 2

使用可能

メモリハードウェアアーキテクチャ

最新のメモリ ハードウェア アーキテクチャは、Java の内部メモリ モデルとは異なります。したがって、Java モデルがハードウェア アーキテクチャでどのように動作するかを知るためには、ハードウェア アーキテクチャを理解する必要があります。このセクションでは一般的なメモリ ハードウェア アーキテクチャについて説明し、次のセクションでは Java がメモリ ハードウェア アーキテクチャでどのように動作するかを説明します。

以下は、最新のコンピューターのハードウェア アーキテクチャの簡略図です。

メモリハードウェアアーキテクチャ

現代の世界では、コンピューターには 2 つ以上のプロセッサーが搭載されており、これはすでに標準となっています。これらのプロセッサーの中には、複数のコアを備えているものもあります。このようなコンピュータでは、複数のスレッドを同時に実行できます。各プロセッサ コアは、常に 1 つのスレッドを実行できます。これは、Java アプリケーションは事前にマルチスレッド化されており、プログラム内ではプロセッサ コアごとに 1 つのスレッドを同時に実行できることを意味します。

プロセッサ コアには、メモリ (コア内部) に常駐する一連のレジスタが含まれています。レジスタ データに対する操作は、コンピュータのメイン メモリ (RAM) にあるデータに対する操作よりもはるかに高速に実行されます。これは、プロセッサがこれらのレジスタに非常に高速にアクセスできるためです。

各 CPU は独自のキャッシュ層を持つこともできます。最新のプロセッサにはこれが搭載されています。プロセッサは、メイン メモリよりもはるかに高速にキャッシュにアクセスできますが、内部レジスタほど高速ではありません。キャッシュのアクセス速度は、メインメモリと内部レジスタのアクセス速度のほぼ中間の値になります。

さらに、プロセッサにはマルチレベル キャッシュを搭載する場所があります。ただし、Java メモリ モデルがハードウェア メモリとどのように対話するかを理解するために、これを知ることはそれほど重要ではありません。プロセッサにはある程度のレベルのキャッシュがある可能性があることを知っておくことが重要です。

どのコンピュータにも同様にRAM(主記憶領域)が搭載されています。すべてのコアがメイン メモリにアクセスできます。メイン メモリ領域は通常、プロセッサ コアのキャッシュ メモリよりもはるかに大きくなります。

プロセッサは、メイン メモリにアクセスする必要があるときに、その一部をキャッシュ メモリに読み取ります。また、キャッシュから一部のデータを内部レジスタに読み取り、それらのレジスタに対して操作を実行することもできます。CPU が結果をメイン メモリに書き戻す必要がある場合、データを内部レジスタからキャッシュにフラッシュし、ある時点でメイン メモリにフラッシュします。

キャッシュに格納されたデータは、通常、プロセッサがキャッシュに何か別のものを格納する必要がある場合、メイン メモリにフラッシュ バックされます。キャッシュには、メモリのクリアとデータの書き込みを同時に行う機能があります。プロセッサは、更新中に毎回完全なキャッシュを読み書きする必要はありません。通常、キャッシュはメモリの小さなブロックで更新され、それらは「キャッシュ ライン」と呼ばれます。1つまたは複数の「キャッシュライン」をキャッシュメモリに読み込むことができ、1つまたは複数のキャッシュラインをメインメモリにフラッシュバックすることができます。

Java メモリ モデルとメモリ ハードウェア アーキテクチャの組み合わせ

すでに述べたように、Java メモリ モデルとメモリ ハードウェア アーキテクチャは異なります。ハードウェア アーキテクチャでは、スレッド スタックとヒープが区別されません。ハードウェアでは、スレッド スタックと HEAP (ヒープ) はメイン メモリに存在します。

スタックやスレッド ヒープの一部が、CPU のキャッシュや内部レジスタに存在する場合があります。これを図に示します。

スレッドスタックとHEAP

オブジェクトと変数がコンピュータのメモリの異なる領域に格納される場合、特定の問題が発生する可能性があります。主なものは次の 2 つです。

  • スレッドが共有変数に加えた変更の可視性。
  • 共有変数の読み取り、チェック、書き込み時の競合状態。

これらの両方の問題については、以下で説明します。

共有オブジェクトの可視性

volatile 宣言や同期を適切に使用せずに 2 つ以上のスレッドがオブジェクトを共有する場合、1 つのスレッドによって行われた共有オブジェクトへの変更は他のスレッドには表示されない可能性があります。

共有オブジェクトが最初はメイン メモリに格納されていると想像してください。CPU 上で実行されているスレッドは、共有オブジェクトを同じ CPU のキャッシュに読み取ります。そこで彼はオブジェクトに変更を加えます。CPU のキャッシュがメイン メモリにフラッシュされるまで、共有オブジェクトの変更されたバージョンは、他の CPU で実行されているスレッドには表示されません。したがって、各スレッドは共有オブジェクトの独自のコピーを取得でき、各コピーは個別の CPU キャッシュに存在します。

次の図は、この状況の概要を示しています。左側の CPU で実行されている 1 つのスレッドは、共有オブジェクトをそのキャッシュにコピーし、count の値を 2 に変更します。 count の更新がまだメイン メモリにフラッシュされていないため、この変更は右側の CPU で実行されている他のスレッドには見えません。

この問題を解決するには、変数を宣言するときに volatile キーワードを使用します。これにより、指定された変数がメイン メモリから直接読み取られ、更新時に常にメイン メモリに書き戻されることが保証されます。

競合状態

2 つ以上のスレッドが同じオブジェクトを共有し、複数のスレッドがその共有オブジェクト内の変数を更新すると、競合状態が発生する可能性があります。

スレッド A が共有オブジェクトのカウント変数をプロセッサのキャッシュに読み取ると想像してください。また、スレッド B が同じことを別のプロセッサのキャッシュで実行すると想像してください。ここで、スレッド A は count の値に 1 を加え、スレッド B も同じことを行います。これで、変数は 2 回増加しました (各プロセッサのキャッシュ内で個別に +1 ずつ)。

これらの増分が連続して実行されると、count 変数は 2 倍になり、メイン メモリに書き戻されます (元の値 + 2)。

ただし、適切な同期が行われずに 2 つの増分が同時に実行されました。どちらのスレッド (A または B) がその更新バージョンの count をメイン メモリに書き込むかに関係なく、2 つの増分にもかかわらず、新しい値は元の値より 1 増えるだけです。

この図は、上で説明した競合状態の問題の発生を示しています。

この問題を解決するには、Java 同期ブロックを使用します。同期ブロックにより、常に 1 つのスレッドだけがコードの特定の重要なセクションに入ることができます。

また、同期ブロックは、同期ブロック内でアクセスされるすべての変数がメイン メモリから読み取られることを保証し、スレッドが同期ブロックを終了すると、変数が volatile または No と宣言されているかどうかに関係なく、更新されたすべての変数がメイン メモリにフラッシュ バックされます。

コメント
  • 人気
  • 新規
  • 古い
コメントを残すには、サインインしている必要があります
このページにはまだコメントがありません