マルチスレッド。Javaメモリモデル(パート2)

こんにちは、Habr!JakobJenkovによる記事 JavaMemoryModel の第2部の翻訳を紹介します。最初の部分はここにあります



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



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



最新のコンピュータのハードウェアアーキテクチャの簡略図を次に示します。



最新のコンピュータには、多くの場合、2つ以上のプロセッサがあります。これらのプロセッサの中には、複数のコアを備えているものもあります。このようなコンピューターでは、複数のスレッドを同時に実行することができます。各プロセッサ(翻訳者注-以下、著者はおそらくプロセッサコアまたはプロセッサによるシングルコアプロセッサを意味します)いつでも1つのスレッドを実行できます。つまり、Javaアプリケーションがマルチスレッドの場合、プログラム内でプロセッサごとに1つのスレッドを同時に実行できます。



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



各CPUはキャッシュレイヤーを持つこともできます。実際、最近のほとんどのプロセッサにはそれがあります。プロセッサは、メインメモリよりもはるかに高速にキャッシュメモリにアクセスできますが、通常、内部レジスタほど高速ではありません。したがって、キャッシュメモリへのアクセス速度は、内部レジスタとメインメモリへのアクセス速度の間のどこかにあります。一部のプロセッサには階層型キャッシュがある場合がありますが、Javaメモリモデルがハードウェアメモリとどのように相互作用するかを理解するために、これを理解することは重要ではありません。プロセッサはある程度のキャッシュメモリを持つことができることを知っておくことが重要です。



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



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



キャッシュに保存されたデータは、通常、プロセッサがキャッシュに他の何かを保存する必要があるときにメインメモリにフラッシュバックされます。キャッシュは、メモリをクリアすると同時に新しいデータを書き込むことができます。プロセッサは、更新されるたびにフルキャッシュを読み書きする必要はありません。通常、キャッシュは「キャッシュライン」と呼ばれるメモリの小さなブロックで更新されます。1つ以上のキャッシュラインをキャッシュメモリに読み込むことができ、1つ以上のキャッシュラインをメインメモリにフラッシュバックすることができます。



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



前述のように、Javaメモリモデルとメモリハードウェアアーキテクチャは異なります。ハードウェアアーキテクチャは、スレッドスタックとヒープを区別しません。ハードウェアでは、スレッドスタックとヒープはメインメモリにあります。スタックとスレッドヒープの一部が、CPUのキャッシュと内部レジスタに存在する場合があります。これを図に示し



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

•共有変数に対してスレッドによって行われた変更の可視性。

•共有変数の読み取り、チェック、および書き込み時のレース条件。

これらの問題の両方について、次のセクションで説明します。



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



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



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



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



この問題を解決するために volatile、変数を宣言するときに使用できます。特定の変数がメインメモリから直接読み取られ、更新時に常にメインメモリに書き戻されるようにすることができます。



レースコンディション



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



スレッドAがcount共有オブジェクト変数をプロセッサのキャッシュに読み込んでいる想像してください。スレッドBが同じことをしているが、異なるプロセッサのキャッシュに対して行っていることも想像してみてください。ここで、スレッドAは変数の値に1を追加し、countスレッドBも同じことを行います。今ではvar12倍に増加してます-別々に、各プロセッサのキャッシュで+1します。



これらの増分が順次実行された場合、変数countは2倍になり、メインメモリに書き戻されます + 2

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



この図は、上記のレース条件の問題の発生を示してい



ます。この問題を解決するには、同期されたJavaブロックを使用できます。..。同期されたブロックにより、常に1つのスレッドのみがコードの特定の重要なセクションに入ることができます。同期ブロックは、同期ブロック内でアクセスされるすべての変数がメインメモリから読み取られることも保証し、スレッドが同期ブロックを終了すると、変数が宣言されているかどうかに関係なく、更新されたすべての変数がメインメモリにフラッシュバックされますvolatile。 ..。



All Articles