JMM(Java メモリモデル)
JMM(Java 内存模型)は、共有変数に対して、別のスレッドがその共有変数へ書き込みを行った後、その変数がそのスレッドに対してどのように可視であるかを定義します。
JMM(Java メモリモデル)を徹底的に理解するには、まず**CPU キャッシュモデルと命令再配置(リオーダリング)**から始める必要があります!
CPU キャッシュモデルから始める
なぜCPUの高速キャッシュを持つのか? Webサイトのバックエンドシステムで使うキャッシュ(例えば Redis)を例に挙げると、プログラムの処理速度と従来の関係型データベースのアクセス速度の差を解消するためです。CPUキャッシュは、CPUの処理速度とメモリの処理速度の不一致を解消するためのものです。
私たちはしばしばメモリを外部記憶装置の高速キャッシュとして見ることができます。プログラム実行時に外部記憶のデータをメモリにコピーします。メモリの処理速度は外部記憶より遥かに速いため、処理速度が向上します。
結論:CPUキャッシュはメモリデータをキャッシュしてCPUの処理速度とメモリの不一致を解消するため、メモリキャッシュはハードディスクデータをキャッシュしてディスクアクセス速度の遅さを解消するためのものです。

現代の CPU Cache は通常3層に分かれており、それぞれL1、L2、L3 Cache と呼ばれます。中には L4 Cache を持つ CPU もあります。
CPU Cache の動作原理: 最初にデータを CPU Cache にコピーします。CPU がデータを必要とするときは CPU Cache から直接読み取り、演算が終わったら演算結果を Main Memory に書き戻します。しかし、これにはメモリキャッシュの不整合性の問題が生じます。例えば、私が i++ を実行した場合、2つのスレッドが同時に実行すると、両方のスレッドが CPU Cache から i=1 を読み取り、両方が i++ を実行して Main Memory へ書き戻すと i=2 になってしまい、正しい結果は i=3 になることもあります。
CPU はこのメモリキャッシュ不整合問題を解決するため、キャッシュ一貫性プロトコル(例えば MESI プロトコル)などを用います。 このキャッシュ一貫性プロトコルは、CPU の高速キャッシュと主メモリの間のやり取りを行う際に従うべき原則・規約を指します。CPU によって使用されるキャッシュ一貫性プロトコルは通常異なります。
私たちのプログラムは OS の上で動作しており、OS は下位ハードウェアの操作の詳細を隠蔽し、さまざまなハードウェア資源を仮想化します。したがって、OS も同様にメモリキャッシュの不整合問題を解決する必要があります。
OS は Memory Model(Memory Model)によって一連の規範を定義してこの問題を解決します。Windows でも Linux でも、それぞれ特有のメモリモデルがあります。
命令再配置
CPU キャッシュモデルの話を終えたら、次に重要な概念である**命令再配置(リオーダリング)**を見ていきます。
実行速度/性能を向上させるため、コンピュータはプログラムコードを実行する際に命令を再配置します。
命令再配置とは何か? 簡単に言えば、コードを実行する際、書いた順序どおりに逐次実行されるとは限りません。
よくある命令再配置には次の2つがあります:
- コンパイラ最適化再配置:コンパイラ(JVM、JIT コンパイラなどを含む)は、単一スレッドのプログラムの意味を変更しない前提のもと、文の実行順序を再配置します。
- 命令並列再配置:現代のプロセッサは命令レベル並列性(ILP)を用いて複数の命令を重複実行します。データ依存性がなければ、文に対応する機械命令の実行順序を変更できます。
また、メモリシステムにも「再配置」が発生しますが、それは厳密には再配置とは言えません。JMM では、主メモリとローカルメモリの内容が一致しない可能性があり、これがマルチスレッドの実行に問題を生じさせます。
Java のソースコードはコンパイラ最適化再配置 → 命令並列再配置 → メモリシステム再配置の順に経て、最終的に OS が実行可能な命令列へと変換されます。
命令再配置はシリアル意味論の一貫性を保証しますが、マルチスレッド間の意味論が必ずしも一貫することを保証する義務はありません。 したがって、マルチスレッドでは命令再配置が問題を引き起こすことがあります。
コンパイラとプロセッサの命令再配置の扱いは異なります。コンパイラの場合、特定のタイプの再配置を禁止することで再配置を抑制します。プロセッサの場合、Memory Barrier(メモリバリア、時には Memory Fence)を挿入して特定の種類のプロセッサ再配置を禁止します。命令並列再配置とメモリシステム再配置は、いずれもプロセサレベルの命令再配置に該当します。
Memory Barrier(メモリ障壁、時には Memory Fence)とは、CPU の命令で、プロセッサの命令の再配置を禁止し、命令の実行の有順序性を保証します。さらに、屏障の効果を得るために、書き込み/読み取りの前に主メモリの値を高速キャッシュへ書き込み、無効なキューをクリアして、変数の可視性を保証します。
JMM(Java Memory Model)
何が JMM か?なぜ JMM が必要か?
Java はメモリモデルを提供することを試みた最も早いプログラミング言語です。初期のメモリモデルには欠陥があり(特にコンパイラの最適化を弱くする要因になりやすい点など)、Java5 以降、Java は新しいメモリモデル 《JSR-133:Java Memory Model and Thread Specification》 を採用しました。
一般には、プログラミング言語はOSレイヤのメモリモデルを直接再利用することもできます。しかし、OSごとにメモリモデルが異なる場合があり、同じコードが別のOSで動作しなくなる可能性があります。Java はクロスプラットフォーム言語であるため、システム差を吸収するためのメモリモデルを自ら提供します。
これは JMM が存在する理由の一つです。実際には、Java にとって JMM は、並列プログラミングに関連する一連の規範を定義するものと見なせます。スレッドと主内存の関係を抽象化するだけでなく、Java のソースコードから CPU が実行可能な命令へと変換される過程で従うべき並行性関連の原則・規範を定め、その主な目的はマルチスレッドプログラミングを簡素化し、プログラムの移植性を高めることです。
なぜこれらの並行関連の原則と規範を守るのか? これは、並行プログラミングの下で、CPU の多段キャッシュや命令再配置といった設計がプログラムの実行に問題を引き起こす可能性があるためです。前述の命令再配置がマルチスレッドプログラムの実行を不適切にする可能性があるため、JMM は happens-before 原則(後述で詳説します)を抽象化してこの問題を解決します。
JMM は要するに、これらの問題を解決するための規範を定義しており、開発者はこの規範を活用してマルチスレッドプログラムをより容易に開発できます。Java の開発者にとっては、底層の原理を理解する必要はなく、直接、volatile、synchronized、さまざまな Lock など、並行性に関連するキーワードやクラスを使用して、並行安全なプログラムを開発できます。
JMM はどのようにスレッドと主内存の関係を抽象化するのか?
Java メモリモデル(JMM) は、スレッドと主内存の関係を抽象化します。例えば、スレッド間で共有変数は主内存に格納されるべきです。
JDK1.2以前は、Java のメモリモデルの実装は常に「主存(共有メモリ)」から変数を読み取るだけで、特別な配慮は必要ありませんでした。しかし、現在の Java メモリモデルでは、スレッドは変数を「ローカルメモリ」(例えば機械のレジスタ)に保存することができ、主内存での直接の読み書きではありません。これにより、あるスレッドが主内存で変数の値を変更したとしても、別のスレッドがまだ自分のレジスタ内のコピーを使い続け、データの不整合を招く可能性があります。
これは前述の CPU キャッシュモデルと非常に似ています。
主内存とは何か?ローカル内存とは何か?
- 主内存:すべてのスレッドが作成するインスタンスオブジェクトは主内存に格納されます。メンバー変数、ローカル変数、クラス情報、定数、静的変数などは主内存に格納されます。高速化のため、仮想機械とハードウェアは作業メモリをレジスタや高速キャッシュに優先的に格納することがあります。
- ローカル内存:各スレッドには私有のローカルメモリがあり、ここにはそのスレッドが共有変数を読み書きするコピーが格納されます。各スレッドは自分のローカル内存内の変数しか操作できず、他のスレッドのローカル内存には直接アクセスできません。スレッド間の通信が必要な場合は主内存を介します。ローカル内存は JMM が抽象的に示した概念で、実在しません。キャッシュ、書き込みバッファ、レジスタ、その他のハードウェア・コンパイラ最適化を含みます。
Java メモリモデルの抽象図は以下のとおりです:

上の図を見ると、スレッド1とスレッド2の間で通信を行う場合、以下の2つのステップを経る必要があります:
- スレッド1 がローカルメモリで修正した共有変数のコピーの値を主内存に同期します。
- スレッド2 が主メモリから対応する共有変数の値を読み取ります。
すなわち、JMM は共有変数の可視性を保証します。
ただし、マルチスレッドでは、主内存の共有変数を操作するとスレッドセーフの問題を引き起こすことがあります。例えば:
- スレッド1 とスレッド2 が同じ共有変数を別々に操作し、1つは値を変更し、もう1つは読み取る。
- スレッド2 が読み取る値が、スレッド1 が変更する前の値なのか、変更後の値なのかは不確実です。どちらも起こり得ます。なぜなら、スレッド1とスレッド2はともに、共有変数を主内存から自分のワークメモリにコピーしているからです。
主内存とワークメモリの具体的な相互作用プロトコル、すなわち変数を主内存からワークメモリへコピーし、ワークメモリから主内存へ同期する実装の詳細について、Java メモリモデルは以下の8つの同期操作を定義します(理解しておけば十分で、丸暗記は不要です):
- lock(ロック):主内存上の変数に作用し、それをスレッドの独占変数としてマークします。
- unlock(アンロック):主内存上の変数に作用し、その変数のロック状態を解除します。ロックが解除された変数は他のスレッドによってロックされることができます。
- read(読み取り):主内存上の変数に作用し、その変数の値を主内存からスレッドのワークメモリへ転送して、後続の load 操作で使用します。
- load(ロード):read 操作で主内存から得た変数値を、ワークメモリ内の変数のコピーに置きます。
- use(使用):ワークメモリ内の変数の値を実行エンジンに渡します。仮想マシンが変数を使用する命令に遭遇するたびにこの操作を行います。
- assign(代入):ワークメモリの変数に作用し、実行エンジンから受け取った値をワークメモリの変数へ代入します。仮想マシンが変数へ値を代入するバイトコード指令に遭遇するたびにこの操作を実行します。
- store(ストア):ワークメモリの変数に作用し、ワークメモリの変数の値を主内存へ転送して、後続の write 操作で使用します。
- write(書き込み):主内存上の変数に作用し、store 操作でワークメモリから得た値を主内存の変数へ格納します。
この8つの同期操作に加え、これらの同期操作を正しく実行することを保証する以下の同期規則が規定されています(理解しておけば十分で、丸暗記は不要です):
- あるスレッドが、根拠なく(assign 操作が発生したことがない状態で)ワークメモリから主内 memory へデータを同期することは許されません。
- 新しい変数は主内メモリでのみ「誕生」し、ワークメモリ内で初期化されていない(load または assign されていない)変数を直接使用することは許されません。つまり、use と store を実行する前に assign と load を先に実行する必要があります。
- 同じ時刻に、1つの変数に対しては1つのスレッドのみが lock 操作を行えますが、lock 操作は同一スレッドで複数回実行可能です。複数回 lock した場合、同じ回数だけ unlock を実行するときにのみ変数のロックが解かれます。
- もしある変数に lock 操作を実行すると、その変数のワークメモリにある値はクリアされます。実行エンジンがこの変数を使用する前に、再度 load か assign 操作を行って値を初期化する必要があります。
- 事前に lock 操作でロックされていない変数に対して unlock を実行することは許されません。また、他のスレッドによりロックされている変数を unlock することも許されません。
Java 内存区域と JMM の違いは?
Java メモリ区域とメモリモデルは、全く別の2つの概念です。
- JVM 内のメモリ構造は、Java 仮想マシンの実行時エリアに関連し、JVM が実行時にデータをどのように領域分割して格納するかを定義します。たとえば、ヒープはオブジェクトのインスタンスを主に格納します。
- Java メモリモデルは Java の並行プログラミングと関連し、スレッドと主内 Memory の関係を抽象化します。例えば、スレッド間の共有変数は主内 Memory に格納されるべきと規定し、Java のソースコードから CPU が実行可能な命令へと変換される過程で従うべき並行性関連の原則・規範を定め、主な目的はマルチスレッドプログラミングを簡素化し、プログラムの可搬性を高めることです。
happens-before 原則とは?
happens-before という概念は、Leslie Lamport が 1978 年に発表した論文「Time, Clocks and the Ordering of Events in a Distributed System」に端を発します。この論文で Lamport は論理時計の概念を提案し、これが最初の論理時計アルゴリズムとなりました。分散環境では、論理時計の変化を規則の連なりとして定義し、論理時計を用いて分散システム内のイベントの前後関係を判断します。論理時計自体は時間そのものを測定するものではなく、イベント発生の前後関係を区別するだけであり、本質的にはhappens-before関係を定義します。
上記で述べた happens-before の背景は重要ではなく、簡単に理解できれば十分です。
JSR-133 は happens-before の概念を導入して、2つの操作間のメモリ可視性を記述します。
なぜ happens-before 原則が必要か? happens-before 原則の誕生は、プログラマとコンパイラ・プロセッサのバランスを取るためです。プログラマは理解しやすい強いメモリモデルを求め、既定の規則に従ってコードを記述します。コンパイラとプロセッサは、制約を緩くした弱いメモリモデルを追求して性能を最大化します。happens-before の設計思想は非常にシンプルです:
- できるだけ少ない制約で、プログラムの実行結果を変えない範囲で、コンパイラとプロセッサが再配置の最適化を行ってもよい。
- 実行結果を変える再配置には、JMM が禁止する。
下面の図は『Java Concurrency in Practice』の一例です。

happens-before の設計思想を理解したうえで、JSR-133 における happens-before の定義は以下のとおりです:
- ある操作が別の操作に対して happens-before であるなら、最初の操作の実行結果は第2の操作に可視であり、最初の操作の実行順序は第2の操作より前である。
- 2つの操作の間に happens-before 関係が存在しても、Java プラットフォームの具体的な実装が必ず happens-before に指定された順序で実行されるとは限らない。再配置後の実行結果が、happens-before に従って実行した結果と一致すれば、JMM もその再配置を許容する。
次のコードを見てください:
int userNum = getUserNum(); // 1int teacherNum = getTeacherNum(); // 2int totalNum = userNum + teacherNum; // 3- 1 happens-before 2
- 2 happens-before 3
- 1 happens-before 3
1 は 2 に対して happens-before だが、1 と 2 の再配置がコードの実行結果に影響を与えない場合、JMM はコンパイラとプロセッサがこの再配置を実行することを許します。しかし、1 と 2 は 3 を実行する前でなければならず、すなわち 1,2 は 3 に対して happens-before です。
happens-before 原則の意味するところは、ある操作が別の操作の前に発生することではなく、前者の結果が後者に可視であることを表すことです。たとえ2つの操作が同じスレッドにあるかどうかは関係ありません。
例:操作 1 が操作 2 に対して happens-before である場合、操作 1 と操作 2 が同じスレッドにない場合でも、操作 1 の結果は操作 2 に可視です。
happens-before の一般的な規則は何ですか?あなたの理解を述べてください?
happens-before の規則は8つありますが、ここでは重要な5つを押さえてください。全てを覚えるのは難しく、すぐ忘れてしまいます。必要時に参照してください。
- プログラム順序規則:同一スレッド内で、コードの順序で前に書かれた操作は、後に書かれた操作に対して happens-before を持つ。
- アンロック規則:アンロックはロックに対して happens-before を持つ。
- volatile 変数規則:volatile 変数への書き込みは、その volatile 変数を後続で読む操作に対して happens-before を持つ。要するに volatile 変数の書き込みの結果は、それ以降のあらゆる操作に可視となる。
- 推移規則:A が B に対して happens-before で、B が C に対して happens-before なら、A は C に対しても happens-before を持つ。
- スレッド開始規則:Thread オブジェクトの start() は、そのスレッドの各アクションに happens-before を持つ。
もし2つの操作が上記のいずれの happens-before 規則にも当てはまらない場合、それらの操作には順序の保証がなく、JVM はこれらの操作を再配置できます。
happens-before と JMM の関係は?
happens-before と JMM の関係は、次の図ですべて説明できます。

再度見るべき並行プログラミングの三つの重要な特性
原子性
1回の操作、または複数回の操作が、全て実行されるか、あるいは全く実行されずに終わるかのいずれかであり、途中で中断されません。
Java には、synchronized、各種 Lock、さまざまな原子クラスを使って原子性を実現します。
- synchronized と各種 Lock は、いっ時点で1つのスレッドだけがそのコードブロックへアクセスできることを保証します。従って原子性を保証します。各種原子クラスは、CAS(compare and swap)操作を利用して原子操作を保証します(場合によっては volatile または final キーワードも使われます)。
可視性
あるスレッドが共有変数を変更すると、他のスレッドは直ちに変更後の最新値を見ることができます。
Java では、synchronized、volatile、さまざまな Lock を用いて可視性を実現します。
変数を volatile に宣言すると、この変数が共有され、値が頻繁に変わる可能性があることを JVM に示します。したがって、この変数を使用するたびに主メモリから読み取られます。
有序性
命令再配置の問題により、コードの実行順序は、コードを書いたときの順序と必ずしも一致しません。
先述の命令再配置の説明でも触れましたが:
命令再配置はシリアル意味論の一貫性を保証しますが、マルチスレッド間の意味論が必ずしも一貫することを保証する義務はありません。
Java では、volatile キーワードが命令の再配置最適化を禁止します。
この記事が役に立ったときは、ぜひ他の人に共有してください!
一部の情報は古い可能性があります





