『並行プログラミングの芸術』読書ノート第 3 章、まずは基礎から始めます。
基礎がしっかりしていないと、地震が起きたときに山が揺れる。
一、基礎#
1、並行性の 2 つの重要な問題#
スレッド間の通信とスレッド間の同期
スレッド通信メカニズム:
- == 共有メモリ ==:暗黙の通信、明示的な同期
- == メッセージ伝達 ==:明示的な通信、暗黙の同期
Java の並行性は共有メモリモデルを採用しています。
2、Java メモリ構造#
Java のすべてのインスタンスフィールド、静的フィールド、配列要素はヒープメモリに格納されます。ローカル変数、メソッド定義パラメータ、例外処理パラメータはスレッド間で共有されません。
JMM はスレッドと主メモリ間の抽象的な関係を定義しています:スレッド間の共有変数は主メモリに格納され、各スレッドにはプライベートなローカルメモリがあり、ローカルメモリにはそのスレッドの共有変数の読み書きのコピーが格納されています。
ローカルメモリは JMM の抽象概念であり、実際には存在せず、キャッシュ、書き込みバッファ、レジスタ、その他のハードウェアやコンパイラの最適化を含みます。
スレッド A とスレッド B が通信するには、2 つのステップを経る必要があります:
- スレッド A はローカルメモリ A に更新された共有変数を主メモリにフラッシュします。
- スレッド B は主メモリから A が以前に更新した共有変数を読み取ります。
JMM は主メモリと各スレッドのローカルメモリ間の相互作用を制御することで、Java プログラマにメモリの可視性保証を提供します。
3、命令の再配置#
- コンパイラ最適化による再配置。コンパイラは、単一スレッドプログラムの意味を変更することなく、文の実行順序を再配置できます。
- 命令レベルの並列性による再配置。現代のプロセッサは、複数の命令を重ねて実行するために命令レベルの並列性(ILP)技術を採用しています。データ依存性が存在しない場合、プロセッサは文に対応する機械命令の実行順序を変更できます。
- メモリシステムの再配置。プロセッサがキャッシュと読み書きバッファを使用するため、ロードとストア操作が乱雑に実行されるように見えることがあります。
Java が最終的に実行する命令シーケンス:
ソースコード --------》コンパイラ最適化による再配置 -------》命令レベルの並列性による再配置 ----------》メモリシステムの再配置 ---------》最終的な命令シーケンス
最初のものは == コンパイラの再配置 == に属し、後の 2 つは == プロセッサの再配置 == に属します。JMM は特定のタイプのコンパイラ再配置とプロセッサ再配置を禁止することで、プログラマに一貫したメモリの可視性保証を提供します。
コンパイラに対して、JMM のコンパイラは特定のタイプのコンパイラ再配置を禁止します。== プロセッサの再配置に関しては、JMM のプロセッサ再配置ルールは、Java コンパイラが命令シーケンスを生成する際に特定のメモリバリア命令を挿入することを要求します ==。これにより、特定のタイプのプロセッサ再配置が禁止されます。
4、メモリバリアの分類#
load:読み込みバッファ
store:書き込みバッファ
4 種類のメモリバリア:
LoadLoad Barriers
:load1 のデータが load2 およびその後のすべての load 命令の前にロードされることを保証します。StoreStore Barriers
:store1 のデータが他のプロセッサに対する可視性が store2 およびその後のすべてのストア命令の前にあることを保証します。LoadStore Barriers
:load が store のストレージをメモリにフラッシュする前にロードされることを保証します。StoreLoad Barriers
:このバリアの前のすべての命令が完了した後にのみ後の命令が実行されます(オーバーヘッドが大きい)。
5、先行発生(happens-before)#
プログラマが従うべきプログラミング原則
==JMM では、ある操作の実行結果が別の操作に対して可視である必要がある場合、これらの 2 つの操作の間には happens-before 関係が存在しなければなりません。==
いくつかの順序規則(プログラマ向け):
- プログラム順序規則:1 つのスレッド内の各操作は、そのスレッド内の任意の後続操作に対して happens-before です。
- モニターロック規則:ロックの解放は、その後のロックの取得に対して happens-before です。
- volatile 変数規則:volatile フィールドへの書き込みは、その後の任意の volatile フィールドへの読み取りに対して happens-before です。
- 推移性:もし A が B に対して happens-before であり、B が C に対して happens-before であれば、A は C に対して happens-before です。
happens-before は、前の操作(実行結果)が後の操作に対して可視であることを要求するだけであり、前の操作が後の操作の前に実行されなければならないことを意味するものではありません!
前の操作が後の操作に対して可視であることは、前の操作が後の操作の前に実行されなければならないことを意味しませんか?
JMMは、もしAがBに対してhappens-beforeであれば、まず実行結果(Aの実行結果がBに**必ずしも可視である必要はない**)が論理的にhappens-beforeと同じであることを保証し、次にAの順序がBの前にあること(ここで言うのは再配置前の順序であり、後に再配置によってAとBの操作順序が調整されることが許可されます)が必要です。しかし最終的な結果は論理的に一致している必要があるため、表面的には順序通りに実行されているように見えます。
二、再配置#
データ依存性
単一スレッドプログラムでは、制御依存性のある操作の再配置は実行結果を変更しません。しかし、マルチスレッドプログラムでは、制御依存性のある操作の再配置がプログラムの実行結果を変更する可能性があります。
詳細は30ページの例を参照してください。
as-if-serial
どのように再配置しても(コンパイラとプロセッサが並列性を高めるために)、(単一スレッド)プログラムの実行結果は変更されてはなりません。コンパイラ、ランタイム、およびプロセッサはすべて as-if-serial の意味を遵守しなければなりません。
三、順序一致性メモリモデル#
プログラムが正しく同期されている場合、プログラムの実行は順序一致性(Sequentially Consistent)を持ちます。すなわち、プログラムの実行結果はそのプログラムが順序一致性メモリモデルで実行された結果と同じです。
- 1 つのスレッド内のすべての操作は、プログラムの順序に従って実行されなければなりません。
- (プログラムが同期されているかどうかに関わらず)すべてのスレッドは単一の操作実行順序しか見ることができません。順序一致性メモリモデルでは、各操作は原子的に実行され、すぐにすべてのスレッドに可視でなければなりません。
未同期プログラムは順序一致性モデルにおいて、全体の実行順序は無秩序ですが、すべてのスレッドは一貫した全体の実行順序しか見ることができません。 この保証が得られるのは、順序一致性メモリモデル内の各操作がすぐに任意のスレッドに可視である必要があるからです。
JMM にはこの保証がありません。なぜなら、現在のスレッドがローカルメモリに書き込んだデータを主メモリにフラッシュした後でなければ、この書き込み操作は他のスレッドに可視にならないからです。
JMM の処理ロジック:クリティカルセクション内のコードは再配置可能です。JMM はクリティカルセクションを出るときと入るときの 2 つの重要なタイミングで特別な処理を行い、スレッドがこれらの 2 つのタイミングで順序一致性モデルと同じメモリビューを持つようにします。
JMM の具体的な実装の基本方針:プログラムの実行結果を変更しない限り、コンパイラとプロセッサの最適化のためにできるだけ多くの便利な道を開くことです。
順序一致性メモリモデルと JMM の違い
- 順序一致性モデルは単一スレッド内の操作がプログラムの順序で実行されることを保証しますが、JMM は単一スレッド内の操作がプログラムの順序で実行されることを保証しません(例えば、上記の正しく同期されたマルチスレッドプログラムのクリティカルセクション内の再配置)。この点は前述の通り、ここでは繰り返しません。
- 順序一致性モデルはすべてのスレッドが一貫した操作実行順序しか見ることができることを保証しますが、JMM はすべてのスレッドが一貫した操作実行順序を見ることができることを保証しません。この点も前述の通り、ここでは繰り返しません。
- ==JMM は 64 ビットの long 型および double 型変数への書き込み操作が原子的であることを保証しませんが、順序一致性モデルはすべてのメモリの読み書き操作が原子的であることを保証します。==
プロセッサバスの動作メカニズム
コンピュータ内でデータはバスを介してプロセッサとメモリの間で転送されます。プロセッサとメモリ間のデータ転送は、毎回一連のステップを通じて行われます。
バストランザクションには ** 読み取りトランザクション(Read Transaction)と書き込みトランザクション(Write Transaction)** が含まれます。読み取りトランザクションはメモリからプロセッサにデータを転送し、書き込みトランザクションはプロセッサからメモリにデータを転送します。各トランザクションはメモリ内の 1 つまたは複数の物理的に連続した単語を読み書きします。
- バスはバスを同時に使用しようとするトランザクションを同期します。1 つのプロセッサがバストランザクションを実行している間、バスは他のプロセッサや I/O デバイスがメモリの読み書きを実行することを禁止します(これにより、単一のバストランザクション内のメモリの読み書きが原子的であることが保証されます)。
- 一部の 32 ビットプロセッサでは、64 ビットデータへの書き込み操作が原子的であることを要求すると、かなりのオーバーヘッドが発生します。==JVM がこのようなプロセッサ上で実行される場合、64 ビットの long/double 型変数の書き込み操作を 2 つの 32 ビットの書き込み操作に分割して実行する可能性があります。==
注意:JSR-133 以前の古いメモリモデルでは、64 ビットの long/double 型変数の読み書き操作は 2 つの 32 ビットの読み書き操作に分割して実行される可能性がありました。JSR-133 メモリモデル(つまり JDK5 以降)では、64 ビットの long/double 型変数の書き込み操作を 2 つの 32 ビットの書き込み操作に分割して実行することのみが許可され、任意の読み取り操作は JSR-133 において原子的でなければなりません(つまり、任意の読み取り操作は単一の読み取りトランザクション内で実行されなければなりません)。
四、volatile メモリセマンティクス#
volatile 変数の特性:
可視性:volatile変数の読み取りは、常に(任意のスレッド)がこの変数への最後の書き込みを見える。
原子性:任意の単一のvolatile変数の読み取り、書き込みは原子的である(long、doubleを含む)が、volatile++のような複合操作は原子的ではありません。
volatile の書き込み - 読み取りのメモリセマンティクス:
書き込み:volatile 変数に書き込むと、JMM はスレッドに対応する == ローカルメモリ内の共有変数の値を主メモリにフラッシュします ==。
読み取り:volatile 変数を読み取ると、JMM はそのスレッド == に対応するローカルメモリを無効にします ==。スレッドは次に主メモリから共有変数を読み取ります。
volatileの読み取り時には主メモリ内のデータが変更されないことを保証する必要があるため、volatileの読み取りが最初の操作である場合は再配置を実現できません。
volatileの書き込み時には以前のデータが主メモリに正常に書き込まれることを保証する必要があるため、volatileの書き込みが2番目の操作である場合は再配置を実現できません。
通常の読み書きはローカルメモリ内で行われますが、主メモリに書き込まれる可能性があるため、ランダム性があります。
volatile メモリセマンティクスの実装#
volatile のメモリセマンティクスを実現するために、コンパイラはバイトコードを生成する際に、命令シーケンス内にメモリバリアを挿入して特定のタイプのプロセッサ再配置を禁止します。
JMM メモリバリア挿入戦略(保守的!):
- 各
volatile書き込み操作
の前にStoreStore
バリアを挿入します。 - 各
volatile書き込み操作
の後にStoreLoad
バリアを挿入します。 - 各
volatile読み取り操作
の後にLoadLoad
とLoadStore
を挿入します。
なぜvolatileの書き込み操作の前にloadstoreを挿入して通常の読み取りとvolatileの書き込みの間の再配置を避ける必要がないのか?
推測:メモリバリア間には包含関係が存在し、例えばstoreloadは他の3つのすべての機能を実現できます。
volatile の書き込み - 読み取りメモリセマンティクスの一般的な使用パターンは、1 つの書き込みスレッドが volatile 変数を書き込み、複数の読み取りスレッドが同じ volatile 変数を読み取ることです。読み取りスレッドの数が書き込みスレッドを大幅に上回る場合、volatile の書き込み後に StoreLoad バリアを挿入することは、実行効率の大幅な向上をもたらします。
実際の状況では、コンパイラがバイトコードを生成する際にメモリバリアを追加するかどうかを最適化できますが、一般的に最後の storeload は省略できません。なぜなら、return の後に次の volatile の読み取り / 書き込みが発生するかどうかを判断できないからです。
異なるプロセッサプラットフォームもメモリバリアに対して最適化を行います。
機能的には、ロックは volatile よりも強力です;スケーラビリティと実行性能の面では、volatile が優れています。
五、ロックのメモリセマンティクス#
ロックの解放と取得のメモリセマンティクス(volatile と同様)
- スレッドがロックを解放すると、ローカルメモリ内の共有変数が主メモリにフラッシュされます(volatile の書き込みに対応)。
- スレッドがロックを取得すると、スレッドに対応するローカルメモリが無効になり、クリティカルセクションのコードは主メモリから共有変数を読み取る必要があります(volatile の読み取りに対応)。
ロックメモリセマンティクスの実装:
ReentrantLock のソースコード分析#
ReentrantLock
の実装は Java 同期器フレームワークAbstractQueuedSynchronizer
(以下AQS
と呼ぶ)に依存しています。AQS
は整型のvolatile
変数(state
と名付けられた)を使用して同期状態を維持します。このvolatile
変数はReentrantLock
のメモリセマンティクス実装の鍵です。
ReentrantLock のロックは公平ロックと非公平ロックに分かれます。
公平ロック
現在、tryAcquireメソッドには少し変更があり、ロックを取得する最初のオブジェクトを確認する際にhasQueuedPredecessors()メソッドが追加され、これは現在のスレッドよりも長い待機時間を持つスレッドがあるかどうかを確認するためのものです。
非公平ロック
CAS を呼び出します:現在の状態値が期待値と等しい場合、原子的に同期状態を指定された更新値に設定します。
公平ロックと非公平ロックのセマンティクスの要約:
- 公平ロックと非公平ロックの解放時には、== 最後に volatile 変数 state を書き込みます。==
- 公平ロックの取得時には、== 最初に volatile 変数を読み取ります。==
- 非公平ロックの取得時には、最初に ==CAS を使用して volatile 変数を更新します。この操作は volatile の読み取りと volatile の書き込みのメモリセマンティクスを同時に持ちます。==
見ると、ロックの解放 - 取得のメモリセマンティクスの実装には少なくとも以下の 2 つの方法があります。
- volatile 変数の書き込み - 読み取りが持つメモリセマンティクスを利用する。
- CAS が付随する volatile の読み取りと volatile の書き込みのメモリセマンティクスを利用する。
CAS はどのように同時に volatile の読み取りと volatile の書き込みのメモリセマンティクスを持つのか?
マルチプロセッサ環境では、cmpxchg 命令に lock プレフィックスを追加します。単一プロセッサでは追加しません(単一プロセッサは自身の順序一致性を維持します)。
- 以前はバスロックを使用していましたが、後にオーバーヘッドを考慮してキャッシュロックを使用するようになり、lock プレフィックス命令の実行オーバーヘッドが大幅に低下しました。
- lock 命令は再配置を防ぎます。
- 書き込みバッファ内のすべてのデータをメモリにフラッシュします。
上記の 2、3 の点はメモリバリアの効果を持ち、volatile の読み取りと volatile の書き込みのメモリセマンティクスを同時に実現するのに十分です。
concurrent パッケージの一般的な実装パターン#
- ==まず、共有変数を
volatile
として宣言します。== - ==次に、
CAS
の原子条件更新を使用してスレッド間の同期を実現します。== - ==同時に、
volatile
の読み取り、書き込みとCAS
が持つvolatile
の読み取りと書き込みのメモリセマンティクスを組み合わせて、スレッド間の通信を実現します。==
六、final フィールドのメモリセマンティクス#
1、final フィールドの再配置規則#
(1)書き込み:== コンストラクタ内で ==final フィールドに書き込むことと、== その後にこの構築されたオブジェクトの参照を参照変数に代入すること == は、これら 2 つの操作は再配置できません。
(2)読み取り:final フィールドを含むオブジェクトの参照を初めて読み取ることと、== その後に初めてこの final フィールドを読み取ること == は、これら 2 つの操作は再配置できません。
2、final フィールドへの書き込みの再配置規則#
==final フィールドへの書き込みをコンストラクタの外に再配置することを禁止する == ことには 2 つの側面があります:
1、コンパイラ:JMMはコンパイラがfinalフィールドへの書き込みをコンストラクタの外に再配置することを禁止します。
2、プロセッサ:コンパイラはfinalフィールドへの書き込みの後、コンストラクタのreturnの前にStoreStoreバリアを挿入します。このバリアはプロセッサがfinalフィールドへの書き込みをコンストラクタの外に再配置することを禁止します。
上記の規則は、オブジェクトの参照が任意のスレッドに可視になる前に、そのオブジェクトの final フィールドが正しく初期化されていることを保証しますが、通常のフィールドにはこの保証がありません。
3、final フィールドの読み取りの再配置規則#
プロセッサ:1 つのスレッド内で、オブジェクト参照を初めて読み取ることと、そのオブジェクトが含む final フィールドを初めて読み取ることに関して、JMM はプロセッサがこれら 2 つの操作を再配置することを禁止します。
コンパイラ:コンパイラは final フィールドの読み取り操作の前にLoadLoad
バリアを挿入します。
上記の再配置規則は、オブジェクトの final フィールドを読み取る前に、その final フィールドを含むオブジェクトの参照を必ず先に読み取ることを保証します(少数のプロセッサにおける間接依存関係の操作を再配置するため)。
4、final フィールドが参照型の場合#
参照型の場合、final フィールドへの書き込みの再配置規則には以下の制約が追加されます:
コンストラクタ内でfinal参照オブジェクトのメンバーに書き込むことと、その後にコンストラクタの外でこの構築されたオブジェクトの参照を参照変数に代入することは、これら2つの操作は再配置できません。
データ競合のあるスレッドでは可視性を保証できません。
5、なぜ final フィールドはコンストラクタ内から溢れ出すことができないのか#
コンストラクタが返される前に、構築されたオブジェクトの参照は他のスレッドに見えないため、final フィールドはまだ初期化されていない可能性があります(コンストラクタ内で再配置が発生する可能性があります)。
==(参照変数が任意のスレッドに可視になる前に、参照変数が指すオブジェクトの final フィールドは正しく初期化される必要があります。これは final フィールドへの書き込みの再配置規則です)==
七、happens-before#
as-if-serial の意味は、単一スレッドプログラムのプログラマに幻想を与えます:単一スレッドプログラムはプログラムの順序で実行されます。
happens-before 関係は、正しく同期されたマルチスレッドプログラムのプログラマに幻想を与えます:正しく同期されたマルチスレッドプログラムは happens-before で指定された順序で実行されます。
これを行う目的は、プログラムの実行結果を変更せずに、プログラムの実行の並列性をできるだけ高めることです。
完全な happens-before 規則
- プログラム順序規則:1 つのスレッド内の各操作は、そのスレッド内の任意の後続操作に対して happens-before です。
- モニターロック規則:ロックの解放は、その後のロックの取得に対して happens-before です。
- volatile 変数規則:volatile フィールドへの書き込みは、その後の任意の volatile フィールドへの読み取りに対して happens-before です。
- 推移性:もし A が B に対して happens-before であり、B が C に対して happens-before であれば、A は C に対して happens-before です。
- start () 規則:もしスレッド A が操作 ThreadB.start ()(スレッド B を起動)を実行した場合、A スレッドの ThreadB.start () 操作はスレッド B 内の任意の操作に対して happens-before です。
- join () 規則:もしスレッド A が操作 ThreadB.join () を実行し、成功して戻った場合、スレッド B 内の任意の操作はスレッド A が ThreadB.join () 操作から成功して戻るまでに happens-before です。
八、二重チェックロックと遅延初期化#
二重チェックロック
問題の説明:(一般的にはシングルトンパターン)ロックをかける前に非空チェックを行い、ロック後に再度非空チェックを行い、二重チェックロックを実現します。
理由:インスタンスを構築する際に、オブジェクト参照ポインタの操作と初期化操作が再配置される可能性があり、これにより if (instance==null) のときにオブジェクトがすでに作成されたと見なされるが、この時点ではまだ初期化が行われていない可能性があります。
1. オブジェクトのメモリ空間を割り当てる
2. オブジェクトを初期化する
3. instanceがメモリ空間を指すように設定する
4. オブジェクトに初めてアクセスする
3と2は再配置される可能性があり、1342のような問題を引き起こす可能性があります。
最初にシングルトンパターンが並行して発生すると、2 つのオブジェクトが初期化される問題が発生する可能性があります。
後にロックを選択しましたが、ロックは問題を解決できますが、深刻な性能オーバーヘッドが発生します。
後にロックの前に非空チェックを追加することを選択しました。これにより、初回の初期化時のみロックをかけ、以降はロックをかける必要がなくなりますが、
まさにこの非空チェックを追加したために、マルチスレッドがこのチェックを行う際に、読み取り操作が待機時間をスキップしてオブジェクトを直接読み取ることができるようになりましたが、この時点ではロック内の空間の再配置により、オブジェクトがまだ初期化されていない可能性があります。 これにより深刻な結果を引き起こす可能性があります。
解決策:
-
volatile。volatile の意味を利用して再配置を禁止します。シングルトンのレイジーロードパターンでは、インスタンスに volatile 修飾子を追加する必要があります。
利点:== 静的フィールドの遅延初期化を実現できるだけでなく、インスタンスフィールドの遅延初期化も実現できます。==
-
クラス初期化に基づく解決策、スレッドがアクセスする前に初期化を完了させます。
JVM はクラスの初期化段階(つまり、クラスがロードされた後、スレッドが使用する前)にクラスの初期化を実行します。この
クラスの初期化中に、JVM はロックを取得します。このロックは、同じクラスの初期化に対する複数のスレッドを同期させることができます。注:クラス初期化のいくつかの状況 1)Tはクラスであり、T型のインスタンスが作成されます。 2)Tはクラスであり、Tで宣言された静的メソッドが呼び出されます。 3)Tで宣言された静的フィールドに値が代入されます。 4)Tで宣言された静的フィールドが使用され、かつこのフィールドが定数フィールドでない場合。 5)Tはトップレベルクラス(Top Level Class、Java言語仕様を参照)であり、T内部に埋め込まれたassert文が実行されます。
JVM 初期化中の同期プロセス#
初期化段階
-
第 1 段階:Class オブジェクトに対して同期を行い(つまり、Class オブジェクトの == 初期化ロック == を取得し)、クラスまたはインターフェースの初期化を制御します。このロックを取得するスレッドは、現在のスレッドがこの初期化ロックを取得できるまで待機します。
== この時点で、初期化ロックを取得できるのは 1 つのスレッドだけです。==
-
第 2 段階:スレッド A がクラスの初期化を実行し、同時にスレッド B は初期化ロックに対応する
condition
で待機します。初期化 instance プロセス(== 再配置が発生する可能性がありますが、他のスレッドには見えません。==)
- メモリ空間を割り当てる
- 参照変数にコピーする
- オブジェクトを初期化する
-
第 3 段階:スレッド A が初期化完了フラグを設定し、待機しているすべてのスレッドを起こします。
-
第 4 段階:スレッド B がクラスの初期化処理を終了します。
スレッド A は第 2 段階の A1 でクラスの初期化を実行し、第 3 段階の A4 で初期化ロックを解放します。
スレッド B は第 4 段階の B1 で == 同じ初期化ロック == を取得し、第 4 段階の B4 の後にクラスにアクセスを開始します。
-
第 5 段階:スレッド C がクラスの初期化処理を実行します。
スレッド A は第 2 段階の A1 でクラスの初期化を実行し、第 3 段階の A4 でロックを解放します。
スレッド C は第 5 段階の C1 で == 同じロック == を取得し、第 5 段階の C4 の後にクラスにアクセスを開始します。
九、JAVA メモリモデルの概要#
プロセッサメモリモデル#
一般的なプロセッサメモリモデル
-
Total Store Ordering メモリモデル(TSO)
プログラム内の == 書き込み - 読み取り == 操作の順序を緩和します。
-
Total Store Ordering メモリモデル(PSO)
プログラム内の == 書き込み - 書き込み == 操作の順序をさらに緩和します。
-
Relaxed Memory Order メモリモデル(RMO)
プログラム内の == 読み取り - 書き込み == 操作の順序をさらに緩和します。
-
PowerPC メモリモデル
== 読み取り - 読み取り == 操作の順序をさらに緩和します。
すべてのプロセッサメモリモデルは、書き込み - 読み取りの再配置を許可します。理由:それらはすべて書き込みキャッシュを使用しています。
書き込みキャッシュは書き込み - 読み取り操作の再配置を引き起こす可能性があります。
書き込みキャッシュは現在のプロセッサにのみ可視であるため、この特性により、現在のプロセッサは他のプロセッサよりも先に一時的に保存された書き込みを見えることができます。
メモリモデル間の関係#
プロセッサメモリモデルと同様に、実行性能を追求する言語はメモリモデルの設計がより弱くなります。
JMM のメモリ可視性保証#
プログラムのタイプによって分けると
-
単一スレッドプログラム:
メモリ可視性の問題は発生しません。
-
正しく同期されたマルチスレッドプログラム
順序一致性を持ちます(プログラムの実行結果はそのプログラムが順序一致性メモリモデルで実行された結果と同じです)。
-
未同期 / 未正しく同期されたマルチスレッドプログラム
JMM はそれらに最小限の安全性保証を提供します:スレッドが実行中に読み取る値は、以前のあるスレッドが書き込んだ値であるか、デフォルト値(0、null、false)である必要があります。
しかし、最小限の安全性はスレッドが読み取る値が必ずしもあるスレッドの書き込み後の値であることを保証しません。== 最小限の安全性はスレッドが読み取る値が無から生じることはないことを保証しますが、スレッドが読み取る値が必ずしも正しいことを保証するものではありません。==
古いメモリモデルの修正#
JDK1.5 以降、volatile のメモリセマンティクス、final のメモリセマンティクスが強化されました。