目次
前述の章のほとんどの説明は、一度に1つの文または式を実行したコードの動作、つまり1つのスレッドによるコードの動作のみに関係していますが、Java Virtual Machineは一度に多数の実行スレッドをサポートできます。 これらのスレッドは、共有メインメモリー内に存在する値やオブジェクトに対して動作するコードを独立して実行します。 スレッドは、多数のハードウェアプロセッサを持つこと、単一のハードウェアプロセッサをタイムスライスすること、または多くのハードウェアプロセッサをタイムスライスすることによってサポートされることがあります。
スレッドは、Threadクラスで表されます。 スレッドを作成する唯一の方法は、このクラスのオブジェクトを作成することです。各スレッドは、このようなオブジェクトに関連付けられます。 スレッドは、対応するThreadオブジェクトでstart()メソッドが呼び出されると起動します。
スレッドの動作は、特に正しく同期されていない場合、混乱して直感的でない可能性があります。 この章では、マルチスレッドプログラムのセマンティクスについて説明します。マルチスレッドプログラムには、複数のスレッドによって更新される共有メモリーの読み取りによって値が表示される可能性がある規則が含まれています。 この仕様は、様々なハードウェア・アーキテクチャのメモリー・モデルと似ているため、これらのセマンティクスはJavaプログラミング言語メモリー・モデルと呼ばれます。 混乱が生じない場合、これらの規則を単に「メモリモデル」と呼びます。
これらのセマンティクスでは、マルチスレッド・プログラムの実行方法を規定していません。 むしろ、マルチスレッド・プログラムの表示が許可される動作を記述します。 許容される動作のみを生成する実行戦略は、許容可能な実行戦略です。
Javaプログラミング言語は、スレッド間で通信するための複数のメカニズムを提供します。 これらの方法の最も基本的な方法は、同期です。これは、モニターを使用して実装されます。 Javaの各オブジェクトは、スレッドがロックまたはロック解除できるモニターに関連付けられています。 モニターのロックを保持できるスレッドは一度に1つだけです。 そのモニターをロックしようとするほかのスレッドは、そのモニターのロックを取得できるまでブロックされます。 スレッドtは、特定のモニターを複数回ロックできます。各ロック解除は、1つのロック操作の効果を元に戻します。
synchronized文(§14.19)は、オブジェクトへの参照を計算します。その後、そのオブジェクトのモニターに対してロック・アクションを実行しようとし、ロック・アクションが正常に完了するまで処理を続行しません。 ロック・アクションが実行されると、synchronized文の本文が実行されます。 本体の実行が正常に完了した場合、または突然完了した場合、その同じモニターに対してロック解除アクションが自動的に実行されます。
synchronizedメソッド(§8.4.3.6)は、ロック・アクションが呼び出されたときに自動的にロック・アクションを実行します。ロック・アクションが正常に完了するまで、その本体は実行されません。 メソッドがインスタンス・メソッドの場合、そのメソッドが呼び出されたインスタンスに関連付けられたモニター(つまり、メソッドの本体の実行中にthisと呼ばれるオブジェクト)がロックされます。 メソッドがstaticの場合は、メソッドが定義されているクラスを表すClassオブジェクトに関連付けられたモニターがロックされます。 メソッドの本体の実行が通常または突然完了した場合、その同じモニターに対してロック解除アクションが自動的に実行されます。
Javaプログラミング言語では、デッドロック状態を阻止することも、検出する必要もありません。 スレッドが(直接的または間接的に)複数のオブジェクトのロックを保持するプログラムは、デッドロック回避のために従来の手法を使用し、必要に応じて、デッドロックを行わない上位レベルのロックプリミティブを作成する必要があります。
volatile変数の読取りと書込み、java.util.concurrentパッケージでのクラスの使用など、その他のメカニズムでは、同期の代替方法が提供されます。
すべてのオブジェクトには、モニターが関連付けられているだけでなく、待機セットが関連付けられています。 待機セットはスレッドのセットです。
オブジェクトが最初に作成されたとき、その待機セットは空です。 待機セットにスレッドを追加したり、待機セットからスレッドを削除したりする基本アクションはアトミックです。 待機セットは、Object.wait、Object.notifyおよびObject.notifyAllメソッドを介してのみ操作されます。
待機セットの操作は、スレッドの中断ステータスや、中断を処理するThreadクラスのメソッドによっても影響を受けます。 また、他のスレッドをスリープおよび結合するためのThreadクラスのメソッドには、待機アクションおよび通知アクションのプロパティから導出されたプロパティがあります。
待機アクションは、wait()の起動時、またはwait(long millisecs)およびwait(long millisecs, int nanosecs)の時間指定フォームで発生します。
パラメータがゼロのwait(long millisecs)の呼出し、または2つのゼロ・パラメータを持つwait(long millisecs, int nanosecs)の呼出しは、wait()の呼出しと同等です。
スレッドは、InterruptedExceptionをスローせずに戻ると、待機から通常どおり戻ります。
スレッドtをオブジェクトmでwaitメソッドを実行するスレッドとし、nをtによるmに対するロック・アクションの数とし、ロック解除アクションでは一致しません。 次のいずれかが発生します。
nがゼロ(つまり、スレッドtがターゲットmのロックをまだ所有していない)の場合は、IllegalMonitorStateExceptionがスローされます。
これが時間指定の待機であり、nanosecs引数が0-999999の範囲内にないか、millisecs引数が負の場合、IllegalArgumentExceptionがスローされます。
スレッドtが中断されると、InterruptedExceptionがスローされ、tの中断ステータスがfalseに設定されます。
それ以外の場合は、次のシーケンスが発生します。
スレッドtがオブジェクトmの待機セットに追加され、mに対してnロック解除アクションが実行されます。
スレッドtは、mの待機セットから削除されるまで、これ以上の命令を実行しません。 スレッドは、次のいずれかのアクションのために待機セットから削除され、その後再開されます。
待機セットから削除するためにtが選択されているmに対して実行されるnotifyアクション。
mに対して実行されるnotifyAllアクション。
tに対して実行されるinterruptアクション。
これが時間指定待機の場合、tをmの待機セットから削除する内部アクションで、少なくともmillisecsミリ秒にnanosecsナノ秒を加えた後の待機セットがこの待機アクションの開始以降に経過します。
実装による内部処理。 スレッドを待機セットから削除し、明示的な指示なしで再開できるようにする、という「誤ったウェイクアップ」を実行することは推奨されませんが、実装は許可されます。
このプロビジョニングでは、スレッドが保持を待機している論理条件がある場合にのみ終了するループ内でのみwaitを使用するJavaコーディング・プラクティスが必要であることに注意してください。
各スレッドは、待機セットから削除される可能性のあるイベントの順序を決定する必要があります。 その順序は、ほかの順序と一致している必要はありませんが、スレッドはその順序でこれらのイベントが発生したかのように動作する必要があります。
たとえば、スレッドtがmの待機セット内にあり、tの割込みとmの通知の両方が発生した場合、これらのイベントに対する順序が必要です。 割り込みが最初に発生したとみなされた場合、tは最終的にInterruptedExceptionをスローしてwaitから戻り、mの待機セット内の他のスレッド(通知時に存在する場合)が通知を受信する必要があります。 通知が最初に発生したとみなされた場合、tは最終的にwaitから正常に戻され、割込みはまだ保留になります。
スレッドtは、mに対してnロック・アクションを実行します。
スレッドtがステップ2で割り込みのために設定したmの待機から削除された場合、tの中断ステータスはfalseに設定され、waitメソッドはInterruptedExceptionをスローします。
通知アクションは、メソッドnotifyおよびnotifyAllの起動時に発生します。
スレッドtをオブジェクトmでこれらのメソッドのいずれかを実行しているスレッドにし、nをmのtによるロック・アクションの数とし、ロック解除アクションでは一致しません。 次のいずれかが発生します。
nがゼロの場合は、IllegalMonitorStateExceptionがスローされます。
これは、スレッドtがターゲットmのロックをまだ所有していない場合です。
nが0より大きく、これがnotifyアクションの場合、mの待機セットが空でないと、mの現在の待機セットのメンバーであるスレッドuが選択され、待機セットから削除されます。
待機セット内のどのスレッドが選択されているかは保証されません。 この待機セットからの削除により、uの待機アクションでの再開が可能になります。 ただし、再開時のuのロック・アクションは、tがmのモニターを完全にロック解除するまでの間は成功しないことに注意してください。
nが0より大きく、これがnotifyAllアクションの場合、すべてのスレッドがmの待機セットから削除され、再開されます。
ただし、待機の再開時に必要なモニターは一度に1つのみロックされることに注意してください。
中断アクションは、Thread.interruptの起動時に発生し、ThreadGroup.interruptなど、Thread.interruptを順番に起動するように定義されたメソッドも発生します。
tをu.interruptを起動するスレッドとし、一部のスレッドuの場合はtとuが同じ場合もあります。 このアクションにより、uの中断ステータスがtrueに設定されます。
また、待機セットにuが含まれるオブジェクトmが存在する場合、uはmの待機セットから削除されます。 これにより、uは待機アクションで再開できます。この場合、この待機は、mのモニターを再ロックした後、InterruptedExceptionをスローします。
Thread.isInterruptedを起動すると、スレッドの中断ステータスを判断できます。 staticメソッドThread.interruptedは、スレッドによって起動され、独自の中断ステータスを監視およびクリアできます。
前述の仕様により、待機、通知および中断の相互作用に関係する複数のプロパティを決定できます。
スレッドが通知され、待機中に中断された場合、次のいずれかになります。
保留中の割り込みを保持したまま、waitから通常どおり戻されます(つまり、Thread.interruptedの呼出しはtrueを返します)。
InterruptedExceptionをスローしてwaitから戻します。
スレッドは、その割り込みステータスをリセットできず、waitへのコールから正常に戻す場合があります。
同様に、割込みによって通知が失われることはありません。 あるスレッドのセットがオブジェクトmの待機セット内にあり、別のスレッドがmに対してnotifyを実行するとします。 次に、いずれかを実行します。
sの少なくとも1つのスレッドは、waitから通常どおりに返す必要があります。
s内のすべてのスレッドは、InterruptedExceptionをスローしてwaitを終了する必要があります。
スレッドが中断され、notifyを介して呼び出され、そのスレッドがInterruptedExceptionをスローしてwaitから戻った場合は、待機セット内の他のスレッドに通知する必要があります。
Thread.sleepを指定すると、システム・タイマーおよびスケジューラの精度および精度に従って、現在実行中のスレッドが指定の期間中スリープ(一時的に実行を停止)します。 スレッドはモニターの所有権を失いません。また、実行の再開は、スレッドを実行するプロセッサのスケジューリングおよび可用性によって異なります。
Thread.sleepもThread.yieldも同期セマンティクスを持っていないことに注意してください。 特に、コンパイラは、Thread.sleepまたはThread.yieldをコールする前に、レジスタにキャッシュされた書込みを共有メモリーにフラッシュする必要はなく、また、Thread.sleepまたはThread.yieldのコール後にレジスタにキャッシュされた値を再ロードする必要もありません。
たとえば、次の(壊れた)コード部分では、this.doneがvolatile boolean以外のフィールドであると仮定します。
while (!this.done)
Thread.sleep(1000);
コンパイラは、フィールドthis.doneを1回だけ自由に読み込み、ループの実行ごとにキャッシュされた値を再利用できます。 これは、別のスレッドがthis.doneの値を変更した場合でも、ループが終了しないことを意味します。
メモリー・モデルは、プログラムとそのプログラムの実行トレースについて、実行トレースがプログラムの有効な実行であるかどうかを記述します。 Javaプログラミング言語のメモリー・モデルは、実行トレース内の各読取りを調べて、その読取りで検出された書込みが特定のルールに従って有効であることをチェックすることで機能します。
メモリーモデルは、プログラムの可能な動作を記述します。 プログラムのすべての結果実行によって、メモリー・モデルで予測可能な結果が生成されるかぎり、実装は自由に任意のコードを生成できます。
これにより、アクションの順序変更や不要な同期の削除など、実装者が無数のコード変換を実行できる自由度が大幅に向上します。
例17.4-1. 正しく同期されていないプログラムが驚くべき動作を示す可能性がある
Javaプログラミング言語のセマンティクスにより、コンパイラおよびマイクロプロセッサは、矛盾した同期コードと対話できる最適化を実行し、パラドックスに見える動作を生成できます。 ここでは、誤って同期されたプログラムが驚くべき動作を示す可能性がある例をいくつか示します。
たとえば、表17.4-Aに示すプログラム・トレースの例を考えてみます。 このプログラムは、ローカル変数r1およびr2と、共有変数AおよびBを使用します。 最初はA == B == 0です。
表17.4-A. 文の順序変更による驚くべき結果- 元のコード
| スレッド1 | スレッド2 |
|---|---|
1: r2 = A; |
3: r1 = B; |
2: B = 1; |
4: A = 2; |
結果r2 == 2およびr1 == 1が不可能である可能性があります。 直感的に、命令1または命令3のどちらかが実行の最初に来るべきです。 命令1が最初に来る場合は、命令4で書き込みを表示できないはずです。 命令3が最初に来る場合は、命令2で書き込みを表示できないはずです。
ある実行がこの動作を示す場合、命令4が命令1より前に来て、命令2より前に来て、命令4より前に来た命令3より前に来たことがわかります。 これは、その面で、ばかげている。
ただし、単独でのスレッドの実行に影響を与えない場合、コンパイラはどちらのスレッドでも命令を並べ替えることができます。 表17.4-Bのトレースに示すように、命令1を命令2で並べ替えると、r2 == 2およびr1 == 1の結果がどのように発生するかを簡単に確認できます。
表17.4-B. 文の順序変更による驚くべき結果- 有効なコンパイラ変換
| スレッド1 | スレッド2 |
|---|---|
B = 1; |
r1 = B; |
r2 = A; |
A = 2; |
プログラマによっては、この動作が「壊れている」ように見えることがあります。 ただし、このコードは適切に同期されていないことに注意してください。
一つのスレッドに書き込みがあります。
別のスレッドで同じ変数を読み取ると、
書き込みと読み取りは同期によって順序付けされません。
この状況は、データ競合(§17.4.5)の一例です。 コードにデータの競合が含まれている場合、多くの場合、直観的な結果が得られます。
いくつかのメカニズムにより、表17.4-Bの順序を変更できます。 Java Virtual Machine実装のJust-In-Timeコンパイラでは、コードまたはプロセッサを再配置できます。 また、Java Virtual Machine実装が実行されるアーキテクチャのメモリー階層によって、コードが並べ替えられているように見える場合があります。 この章では、コードをコンパイラとして並べ替えることができるすべてのものについて説明します。
驚くべき結果の別の例は、表17.4-Cを参照してください。 最初は、p == qおよびp.x == 0です。 また、このプログラムは誤って同期され、これらの書き込み間の順序付けを強制せずに共有メモリーに書き込みます。
表17.4-C. フォワード置換による驚くべき結果
| スレッド1 | スレッド2 |
|---|---|
r1 = p; |
r6 = p; |
r2 = r1.x; |
r6.x = 3; |
r3 = q; |
|
r4 = r3.x; |
|
r5 = r1.x; |
1つの一般的なコンパイラ最適化では、r5に対してr2の値の読取りが再利用されます。両方ともr1.xの読取りで、書込みが介在しません。 この状況を表17.4-Dに示します。
表17.4-D. フォワード置換による驚くべき結果
| スレッド1 | スレッド2 |
|---|---|
r1 = p; |
r6 = p; |
r2 = r1.x; |
r6.x = 3; |
r3 = q; |
|
r4 = r3.x; |
|
r5 = r2; |
ここでは、スレッド2のr6.xへの割当てが、r1.xの最初の読取りとスレッド1のr3.xの読取りの間に行われる場合について考えてみます。 コンパイラがr5に対してr2の値を再利用することを決定した場合、r2およびr5の値は0になり、r4の値は3になります。 プログラマの観点からは、p.xに格納された値が0から3に変更されてから、再度変更されました。
メモリーモデルは、プログラム内のすべてのポイントで読み取ることができる値を決定します。 単独での各スレッドのアクションは、そのスレッドのセマンティクスによって制御されるとおりに動作する必要があります。ただし、各読取りで認識される値はメモリー・モデルによって決定されます。 これを参照すると、プログラムがスレッド内セマンティクスに従うことになります。 スレッド内セマンティクスは、シングルスレッド・プログラムのセマンティクスであり、スレッド内の読取りアクションによって認識される値に基づいてスレッドの動作を完全に予測できます。 実行中のスレッド tのアクションが妥当かどうかを判断するために、この仕様の休止部分で定義されているシングルスレッドコンテキストで実行されるスレッド tの実装を評価するだけです。
スレッド tの評価によってスレッド間アクションが生成されるたびに、プログラム順序で次に来るスレッド間アクション aの tと一致する必要があります。 aが読取りの場合は、tをさらに評価すると、メモリー・モデルによって決定されたaで検出された値が使用されます。
この項では、§17.5で説明されているfinalフィールドに関する問題を除き、Javaプログラミング言語メモリー・モデルの仕様について説明します。
ここで指定するメモリー・モデルは、基本的にJavaプログラミング言語のオブジェクト指向の性質に基づいていません。 この例の簡潔さと簡潔さのために、クラスやメソッドの定義、明示的な間接参照を行わずにコード・フラグメントを示すことがよくあります。 ほとんどの例は、ローカル変数、共有グローバル変数またはオブジェクトのインスタンス・フィールドにアクセスできる文を含む2つ以上のスレッドで構成されます。 通常、r1やr2などの変数名を使用して、メソッドまたはスレッドに対してローカルな変数を示します。 このような変数には、他のスレッドからはアクセスできません。
スレッド間で共有できるメモリーは、共有メモリーまたはヒープ・メモリーと呼ばれます。
すべてのインスタンス・フィールド、staticフィールドおよび配列要素は、ヒープ・メモリーに格納されます。 この章では、変数という用語は、フィールドと配列要素の両方を指す場合に使用します。
ローカル変数(§14.4)、仮メソッド・パラメータ(§8.4.1)、例外ハンドラ・パラメータ(§14.20)はスレッド間で共有されることはなく、メモリー・モデルによって影響を受けません。
同じ変数への2つのアクセス(読取りまたは書込み)は、少なくとも1つのアクセスが書込みである場合、競合と見なされます。
スレッド間アクションは、あるスレッドによって実行されるアクションで、別のスレッドによって検出または直接影響を受けます。 プログラムが実行できるスレッド間アクションには、いくつかの種類があります。
読取り(標準または非揮発性)。 変数の読取り。
書込み(標準または非揮発性)。 変数の書込み
同期アクション:
揮発性読取り。 変数の揮発性読み取り。
揮発性書込み。 変数の揮発性書込み。
Lock モニターのロック
ロック解除 モニターのロック解除
スレッドの最初と最後のアクション(合成)。
スレッドを開始したり、スレッドが終了したことを検出するアクション(§17.4.4)。
外部アクション。 外部アクションは、実行の外部で監視可能なアクションであり、実行の外部の環境に基づいて結果を持ちます。
スレッド相違処理 (§17.4.9)。 スレッドの相違アクションは、メモリー、同期または外部アクションが実行されない無限ループ内のスレッドによってのみ実行されます。 スレッドがスレッド相違アクションを実行する場合、そのあとに無限数のスレッド相違アクションが続きます。
スレッドの相違アクションが導入され、スレッドによって他のすべてのスレッドが停止し、進行に失敗する可能性がある方法がモデル化されます。
この仕様は、スレッド間アクションにのみ関係します。 スレッド内アクション(たとえば、2つのローカル変数を追加し、その結果を3番目のローカル変数に格納する)を気にする必要はありません。 前述のように、すべてのスレッドは、Javaプログラムの正しいスレッド内セマンティクスに従う必要があります。 通常、スレッド間アクションをより簡潔にアクションと呼びます。
アクションaは、次のタプル< t、k、v、u>によって記述されます。
t - アクションを実行するスレッド
k - アクションの種類
v - アクションに関連する変数またはモニター。
ロック・アクションの場合、vはロックされているモニターで、ロック解除アクションの場合、vはロック解除されているモニターです。
アクションが(揮発性または非揮発性)読み取りの場合、vは読み取られる変数です。
アクションが(揮発性または非揮発性)書き込みの場合、vは書き込まれる変数です。
u - アクションの任意の一意識別子
外部アクション・タプルには、アクションを実行するスレッドによって認識される外部アクションの結果を含む追加のコンポーネントが含まれます。 これは、アクションの成功または失敗に関する情報、およびアクションによって読み取られる任意の値です。
外部アクションのパラメータ(ソケットに書き込まれるバイトなど)は、外部アクション・タプルの一部ではありません。 これらのパラメータは、スレッド内の他のアクションによって設定され、スレッド内セマンティクスを調べることによって決定できます。 これらはメモリー・モデルでは明示的に説明されません。
終了していない実行では、すべての外部アクションが監視できるわけではありません。 終了しない実行および監視可能なアクションについては、§17.4.9で説明します。
各スレッドtによって実行されるすべてのスレッド間アクションの中で、tのプログラム順序は、tのスレッド内セマンティクスに従ってこれらのアクションが実行される順序を反映した合計順序です。
すべてのアクションがプログラム順序と一致する合計順序(実行順序)で発生し、さらに変数vの各読取りrによってvに書き込まれた値がwからvに書き込まれる場合、一連のアクションは順次一貫性があります。
wは、実行順序で rの前に来ます。
wがwの前に、wwが実行順序でrの前に来るような書込みwは他にありません。
順次一貫性は、プログラムの実行における可視性と順序付けに関して非常に強力な保証です。 順次一貫した実行内では、プログラムの順序と一致する個々のアクション(読取りや書込みなど)すべてに合計順序があり、個々のアクションはアトミックであり、すべてのスレッドに即時に表示されます。
プログラムにデータ競合がない場合、プログラムのすべての実行は順次一貫しているように見えます。
データ競合からの順次一貫性または自由度(あるいはその両方)によって、不可分に認識される必要があり、認識されない操作のグループから発生するエラーが依然として許容されます。
メモリー・モデルとして順次整合性を使用する場合、これまでに説明したコンパイラおよびプロセッサの最適化の多くは違法です。 たとえば、表17.4-Cのトレースでは、p.xへの3の書込みが発生するとすぐに、その値を確認するためにその場所の後続の読取りが必要になります。
すべての実行に同期順序があります。 同期順序は、実行のすべての同期アクションの合計順序です。 スレッドtごとに、tの同期アクション(§17.4.2)の同期順序は、tのプログラム順序(§17.4.3)と一致します。
同期アクションは、次のように定義されたアクションに対してsynchronized-withリレーションを誘導します。
モニターm synchronizes-withに対する、mに対する後続のすべてのロックアクションのロック解除アクション(このとき、「subsequent」は同期順序に従って定義されます)。
揮発性変数 v (§8.3.1.4) synchronizes-withへの書き込み(後続の読み取りは同期順序に従って定義されます)。vの以降のすべての読み取り(後続の読み取りは同期順序に従って定義されます)。
スレッドが開始するスレッドの最初のアクションをsynchronizes-with起動するアクション。
各変数へのデフォルト値(ゼロ、falseまたはnull)の書込みは、各スレッドの最初のアクションをsynchronizes-withします。
変数を含むオブジェクトが割り当てられる前に、デフォルト値を変数に書き込むのは少し奇妙に思えるかもしれませんが、概念上、すべてのオブジェクトは、デフォルトの初期化された値を使用してプログラムの開始時に作成されます。
スレッドT1の最後のアクションは、T1が終了したことを検出する別のスレッドT2内のアクションをsynchronizes-withします。
T2は、T1.isAlive()またはT1.join()をコールすることでこれを実現できます。
スレッドT1がスレッドT2を中断した場合、T1によるsynchronizes-withは、他のスレッド(T2を含む)がT2が(InterruptedExceptionをスローするか、Thread.interruptedまたはThread.isInterruptedを呼び出して)中断されたと判断する任意のポイントを割り込みます。
synchronizes-withエッジのソースをリリースと呼び、宛先を取得と呼びます。
happens-before関係では、2つのアクションを順序付けできます。 happens-beforeアクションが別のアクションの場合、最初のアクションは2番目のアクションに対して表示され、2番目のアクションより前に順序付けられます。
2つのアクションxおよびyがある場合は、hb(x、 y)を記述して、x happen-before yを示します。
xとyが同じスレッドのアクションで、xがプログラム順序でyより前になると、hb(x、 y)になります。
オブジェクトのコンストラクタの最後から、そのオブジェクトのファイナライザの開始までのhappens-beforeエッジがあります(§12.6)。
アクションx synchronizes-withに次のアクションyがある場合は、hb(x、 y)もあります。
hb(x、 y)およびhb(y、 z)の場合、hb(x、 z)になります。
クラスObjectのwaitメソッド(§17.2.1)には、それらに関連付けられたロックおよびロック解除アクションがあります。それらのhappens-before関係は、これらの関連アクションによって定義されます。
2つのアクション間にhappens-before関係が存在することは、必ずしも実装内でその順序で実行する必要があることを意味するものではありません。 順序変更によって法的実行と一致する結果が生成される場合、それは違法ではありません。
たとえば、スレッドによって構築されたオブジェクトのすべてのフィールドへのデフォルト値の書込みは、そのスレッドの開始前に行う必要はありません。ただし、読取りによってその事実が観測されることはありません。
具体的には、2つのアクションがhappens-before関係を共有する場合、happens-before関係を共有しないコードに対して、その順序で発生したとはかぎりません。 たとえば、あるスレッドで、別のスレッドでの読み取りと競合している書き込みは、それらの読み取りに対して順不同のように見えることがあります。
happens-beforeリレーションは、データの競合が発生するタイミングを定義します。
一連の同期エッジ(S)は、プログラム順序によるSの推移的クローズによって実行のhappens-beforeエッジがすべて決定されるように、最小セットである場合は十分です。 このセットは一意です。
前述の定義から、次のことを行います。
モニターに対する後続のロックの前に、モニターのロックを解除します。
volatileフィールド(§8.3.1.4)への書込み(happens-before)。
開始されたスレッド内のアクションhappens-beforeスレッドでのstart()のコール。
happen-beforeスレッド内のすべてのアクションは、そのスレッドのjoin()から正常に戻されます。
プログラムの他のアクション(デフォルト書込み以外)の前にオブジェクトを初期化します。
プログラムに2つの競合するアクセス(§17.4.1)が含まれていて、前回の関係では順序付けられていない場合、データ競合が含まれていると言われています。
配列長の読取り(§10.7)、チェックされたキャストの実行(§5.5、§15.16)、仮想メソッドの呼出し(§15.12)など、スレッド間アクション以外の操作のセマンティクスは、データの競合によって直接影響を受けません。
したがって、データの競合によって、配列に誤った長さを返すなどの誤った動作が発生することはありません。
プログラムが正しく同期されるのは、順次一貫性のあるすべての実行でデータの競合が発生していない場合のみです。
プログラムが正しく同期されている場合、プログラムのすべての実行は順次一貫しているように見えます(§17.4.3)。
これはプログラマーにとって非常に強力な保証です。 プログラマは、コードにデータの競合が含まれているかどうかを判断するために、並べ替えを理由にする必要はありません。 したがって、コードが正しく同期されているかどうかを判断する際に、順序変更について理由を付ける必要はありません。 コードが正しく同期されていると判断すると、プログラマは順序変更によってコードが影響を受けることを心配する必要はありません。
コードの順序変更時に監視できる直観的な動作の種類を回避するために、プログラムを正しく同期させる必要があります。 正しい同期を使用すると、プログラムの全体的な動作が正しいとはかぎりません。 ただし、その使用により、プログラマはプログラムの考えられる動作を単純な方法で推論できます。正しく同期されたプログラムの動作は、考えられる順序変更に大きく依存しません。 正しい同期がないと、非常に奇妙でわかりにくく、直観的な動作が可能になります。
変数 vの読み取り rは、実行トレースの happens-before部分的な順序で vへの書き込み wを観察できます。
rは、wの前に順序付けられていません(つまり、hb(r、 w)の場合ではありません)。
vへのwの書込み(つまり、hb(w、 w')およびhb(w'、 r)のようにvへのwの書込みは行いません)。
非公式には、happens-beforeの順序付けがない場合に、読取りrが書込みwの結果を参照して、その読取りを防止できます。
Aの一連のアクションは、happens-before consistentであり、すべての読取りrがAの場合、W(r)はrによって確認された書込みアクションであり、hb(r、W(r))、またはwがAに存在し、w.v = r.vおよびhb(W(r)、w)およびhb(w、r)の場合ではありません。
happens-before consistentアクション・セットでは、各読取りで、happens-before順序付けによって表示が許可されている書込みが認識されます。
例17.4.5-1. 事前の一貫性
表17.4.5-Aのトレースについては、最初はA == B == 0です。 各読取りが適切な書込みを参照できるようにする実行順序があるため、トレースはr2 == 0およびr1 == 0を監視でき、happens-before consistentのままです。
表17.4.5-A. 一貫性の前は発生しますが、連続した一貫性では許可されない動作です。
| スレッド1 | スレッド2 |
|---|---|
B = 1; |
A = 2; |
r2 = A; |
r1 = B; |
同期はないため、各読取りは初期値の書込みまたは別のスレッドによる書込みのいずれかを参照できます。 この動作を表示する実行順序は次のとおりです。
1: B = 1; 3: A = 2; 2: r2 = A; // sees initial write of 0 4: r1 = B; // sees initial write of 0
一貫性を保つ前に発生する別の実行順序は次のとおりです。
1: r2 = A; // sees write of A = 2 3: r1 = B; // sees write of B = 1 2: B = 1; 4: A = 2;
この実行では、後で実行順序で発生する書込みが読取りに表示されます。 これは直感的ではないように思えるかもしれませんが、happens-before一貫性によって許可されます。 読み取りにあとで書き込みを表示させると、許容できない動作が発生することがあります。
実行Eは、タプル< P、 A、 po、 so、 W、 V、 sw、 hb>によって記述され、次のもので構成されます。
P - プログラム
A - 一連のアクション
po - 各スレッドtのプログラム順序は、tによってAで実行されたすべてのアクションの合計順序です。
so - 同期順序。Aのすべての同期アクションの合計順序です。
W - Aの各読取りrに対して、W(r) (Eのrで表示される書込みアクション)を付与する書込み対象関数。
V - Aでwを書き込むたびに、V(w) (Eでwによって書き込まれる値)を与える値書込み関数。
sw - 同期アクションでの部分的な順序による同期
hb - アクションの前に発生する部分的な順序
synchronizes-with要素とhappen-before要素は、実行の他のコンポーネントおよび整形式実行のルールによって一意に決定されることに注意してください(§17.4.7)。
一連のアクションがhappens-before consistent(§17.4.5)の場合、実行はhappens-before consistentです。
整形式の実行のみを考慮します。 実行E = < P、 A、 po、 so、 W、 V、 sw、 hb >は、次の条件を満たす場合に適切に形成されます。
各読取りでは、実行中に同じ変数への書込みが認識されます。
volatile変数のすべての読み取りと書き込みはvolatileアクションです。 Aのすべての読取りrについて、AにW(r)があり、W(r).v = r.vです。 変数 r.vは、rが揮発性読み取りであり、変数 wが揮発性書き込みである場合にのみ揮発性である場合にのみ、r.vは揮発性である。
事前発生オーダーは部分オーダーです。
前処理順序は、エッジと同期の推移的クローズおよびプログラム順序によって指定されます。 有効な部分的な順序(反射、推移および非対称)である必要があります。
実行はスレッド内の一貫性に従います。
各スレッド tについて、A内の tによって実行されるアクションは、プログラム順番でそのスレッドによって個別に生成されるアクションと同じであり、各書き込み wは値 V(w)を書き込みます。ただし、各読み取り rが値 V(W(r))を認識します。 各読取りで表示される値は、メモリー・モデルによって決まります。 指定されたプログラム順序は、Pのスレッド内セマンティクスに従ってアクションが実行されるプログラムの順序を反映する必要があります。
この実行はhappens-before consistent (§17.4.6)です。
実行は同期順序の一貫性に従います。
Aのすべての揮発性読取りrの場合、so(r、 W(r))またはwがAに存在し、w.v = r.vおよびso(W(r)、 w)およびso(w、 r)が書込みr.vの場合ではありません。
f|dは、fのドメインを dに制限することによって与えられた関数を示すために使用します。 dのすべてのx、f|d(x) = f(x)およびxのすべてのd、f|d(x)は未定義です。
p|dは、d内の要素に対する部分的な順序pの制限を表すために使用します。 すべてのx、dのy、p(x、y) (p|d(x、y)の場合のみ。 xまたはyのいずれかがdにない場合、p|d(x、y)には該当しません。
整形式の実行E = < P、 A、 po、 so、 W、 V、 sw、 hb >は、Aからのコミット・アクションによって検証されます。 Aのすべてのアクションをコミットできる場合、実行はJavaプログラミング言語メモリー・モデルの原因要件を満たします。
空のセットからC0として、一連のアクションAからアクションを実行し、コミットされたアクションのセットCiに追加して、コミットされたアクションの新しいセットCi+1を取得します。 これが妥当であることを実証するには、Ciごとに、特定の条件を満たすCiを含む実行Eを示す必要があります。
実行Eは、次のものが存在する場合にのみ、Javaプログラミング言語メモリー・モデルの因果関係の要件を満たします。
次のような一連のアクション C0、C1。
C0は空のセットです
Ciは、Ci+1の適切なサブセットです。
A = U (C0、C1など)
Aが有限の場合、シーケンス C0、C1 ...は有限になり、セット Cn = Aで終わります。
Aが無限の場合、シーケンスC0、C1 ...は無限であり、この無限シーケンスのすべての要素の結合がAと等しい必要があります。
整形式の実行 E1、 ...、 Ei = < P、 Ai、 poi、 soi、 Wi、 Vi、 swi、 hbi >
これらの一連のアクション C0、...、および実行 E1 ...を考慮すると、Ci内のすべてのアクションは Ei内のアクションの1つである必要があります。 Ciのすべてのアクションは、EiとEの両方で、オーダー前と同じ相対的な順序および同期順序を共有する必要があります。 正式に:
CiはAiのサブセットです。
hbi|Ci = hb|Ci
soi|Ci = so|Ci
Ciでの書込みによって書き込まれる値は、EiとEの両方で同じである必要があります。 Ci-1の読取りのみが、EEと同じ書込みをEで表示する必要があります。 正式に:
Vi|Ci = V|Ci
Wi|Ci-1 = W|Ci-1
iでCi-1に含まれていないすべての読取りで、その前に発生した書込みが表示される必要があります。 Ci - Ci-1の各読取りrでは、EiとEEの両方にEの書込みが表示されていますが、Eiの書込みがEに表示される書込みとは異なる場合があります。 正式に:
Ai - Ci-1の読取りhbi(Wi(r)、 r)があります。
(Ci - Ci-1)内のrには、Ci-1内のW(r)およびCi-1内のi(r)があります
Eiの十分なsynchronizes-withエッジがある場合、コミットするアクションの前に発生するリリース取得ペア(§17.4.5)がある場合、そのペアはすべてのEjに存在する必要があります(j ≥ i)。 正式に:
sswiは、hbiの推移的縮小でもあり、poにはない swiエッジになります。 sswiは、Eiの十分な同期とエッジと呼ばれます。 Ciのsswi(x、 y)およびhbi(y、 z)およびCiのswj(x、 y)がすべてのjの≥ iの場合。
アクションyがコミットされると、yの前に実行されるすべての外部アクションもコミットされます。
yがCiにある場合、xは外部アクションで、hbi(x、 y)はCiのxです。
例17.4.8-1. 一貫性が不十分になる前の処理
事前時の一貫性は必要ですが、不十分な制約のセットです。 一貫性を保つ前に発生するだけでは、許容できない動作(プログラムに対して設定した要件に違反する動作)が可能になります。 たとえば、happen-before一貫性を使用すると、「薄い空気から」値を表示できます。 これは、表17.4.8-Aのトレースの詳細な調査によって確認できます。
表17.4.8-A. 事前の一貫性では不十分です。
| スレッド1 | スレッド2 |
|---|---|
r1 = x; |
r2 = y; |
if (r1 != 0) y = 1; |
if (r2 != 0) x = 1; |
表17.4.8-Aに示すコードは、正しく同期されています。 同期アクションは実行されないため、これは驚くべきことです。 ただし、プログラムが順次一貫した方法で実行されるときにデータの競合がない場合、プログラムは正しく同期されることに注意してください。 このコードが順次一貫した方法で実行される場合、各アクションはプログラム順序で発生し、いずれの書き込みも発生しません。 書込みは発生しないため、データの競合は発生しません。プログラムは正しく同期されます。
このプログラムは正しく同期されているため、許容できる動作は順次一貫した動作のみです。 ただし、このプログラムの実行は、一貫性が保たれる前に行われますが、順次一貫性は保たれません。
r1 = x; // sees write of x = 1 y = 1; r2 = y; // sees write of y = 1 x = 1;
この結果は、発生する前に一貫性が保たれます。発生を防止する、発生前の関係はありません。 ただし、この動作が発生する順次一貫した実行はありません。 実行順序の後半で発生した書込みを読取りで確認できるという事実により、許容できない動作が発生することがあります。
読み込みを許可して後で実行順序に入ってくる書き込みを確認することは望ましくない場合もありますが、必要な場合もあります。 前述のとおり、表17.4.5-Aのトレースでは、実行順序の後半で発生した書込みを確認するためにいくつかの読取りが必要です。 読取りは各スレッドで最初に行われるため、実行順序の最初のアクションは読取りである必要があります。 その読み取りで、あとで発生した書き込みが表示されない場合は、読み取った変数の初期値以外の値を表示できません。 これは明らかにすべての行動を反映しているわけではありません。
表17.4.8-Aのような場合に発生する問題のために、読取りで将来の書込みが原因として表示される場合の問題を参照します。 その場合、読取りによって書込みが発生し、書込みによって読取りが発生します。 行動に「第一の原因」はない。 したがって、メモリー・モデルでは、どの読取りが早期に書込みを参照できるかを判断する一貫した方法が必要です。
表17.4.8-Aに示すような例は、実行の後半で発生した書込みを読取りが参照できるかどうかを示す際、仕様に注意する必要があることを示しています(実行の後半で発生した書込みが読取りで検出された場合、その書込みが実際に早期に実行されることを念頭に置いてください)。
メモリー・モデルは、入力として指定された実行とプログラムを取得し、その実行がプログラムの有効な実行であるかどうかを判断します。 これは、プログラムによって実行されたアクションを反映する「コミットされた」アクションのセットを徐々に構築することによって行います。 通常、次にコミットされるアクションには、順次一貫した実行によって実行できる次のアクションが反映されます。 ただし、後の書込みを確認する必要がある読取りを反映するために、一部のアクションを、その前に発生する他のアクションよりも早くコミットできます。
明らかに、いくつかのアクションは早期に実行され、一部は実行されない可能性があります。 たとえば、表17.4.8-Aの書込みの1つが、その変数の読取り前にコミットされた場合は、読取りによって書込みが確認され、空気外の結果が発生する可能性があります。 非公式には、データの競合が発生すると仮定せずにアクションが発生する可能性があることがわかっている場合、アクションを早期にコミットできます。 表17.4.8-Aでは、データの競合の結果が読取りで確認されないかぎり、書込みは実行できないため、早期に書込みを実行できません。
制限された有限期間内に常に終了するプログラムの場合、その動作は、許容される実行に関して単純に(公式に)理解できます。 制限された時間内に終了できないプログラムの場合、より微妙な問題が発生します。
プログラムの観察可能な動作は、プログラムが実行できる外部アクションの有限セットによって定義されます。 たとえば、「Hello」を永久に出力するプログラムは、負でない整数 iに対して「Hello」を i回出力する動作を含む一連の動作によって記述されます。
終了は動作として明示的にモデル化されませんが、プログラムを簡単に拡張して、すべてのスレッドが終了したときに発生する追加の外部アクションexecutionTerminationを生成できます。
また、特別なハング・アクションも定義します。 動作がハング・アクションを含む一連の外部アクションによって記述されている場合、外部アクションが観察された後に、プログラムは追加の外部アクションを実行したり終了することなく、無制限の時間実行できる動作を示します。 すべてのスレッドがブロックされている場合、またはプログラムが外部アクションを実行せずに無制限の数のアクションを実行できる場合は、プログラムをハングアップできます。
スレッドは、ロックを取得しようとしたり、外部データに依存する外部アクション(読み取りなど)を実行しようとする場合など、さまざまな状況でブロックできます。
実行によって、スレッドが無期限にブロックされ、実行が終了しない場合があります。 このような場合、ブロックされたスレッドによって生成されるアクションは、スレッドがブロックされる原因となったアクションまで、そのスレッドによって生成されたすべてのアクションと、そのアクションの後にスレッドによって生成されるアクションを含めて構成する必要があります。
観察可能な行動を推論するには、一連の観察可能な行動について話す必要があります。
Oが実行Eの観察可能なアクションのセットである場合、OはEのアクション(A)のサブセットである必要があり、Aに無限の数のアクションが含まれている場合でも、有限数のアクションのみを含める必要があります。 さらに、アクションyがOにあり、hb(x、 y)またはso(x、 y)のいずれかである場合、xはOになります。
一連の監視可能なアクションは、外部アクションに制限されないことに注意してください。 むしろ、観察可能な一連のアクションに含まれる外部アクションのみが、観察可能な外部アクションとみなされます。
動作Bは、Bが外部アクションの有限セットであり、次のいずれかの場合にのみ、プログラムPの許容可能な動作です。
Pの実行Eが存在し、Eに対する監視可能なアクションのセットOが存在し、BはO内の外部アクションのセットです(E内のスレッドがブロックされた状態で終了し、OにはE内のすべてのアクションが含まれ、Bにはhangアクションが含まれる場合もあります)。または
Bがハング・アクションとOのすべての外部アクション、およびkのすべてのO ≥ | O |の実行E/PとアクションAのすべての外部アクションで構成されるアクションが存在し、次のような一連のアクションOが存在します。
OとOは、監視可能なアクション・セットの要件を満たすAのサブセットです。
O' 閉じ A
| O' | ≥ k
O - Oに外部アクションが含まれていない
動作Bでは、Bの外部アクションが観察される順序は説明されませんが、外部アクションの生成方法および実行方法に関するその他の(内部)制約では、このような制約が課される可能性があります。
finalフィールド・セマンティクス
finalと宣言されたフィールドは、一度初期化されますが、通常の状況では変更されません。 finalフィールドの詳細なセマンティクスは、通常のフィールドとは多少異なります。 特に、コンパイラは、finalフィールドの読取りを同期の障壁を越えて移動したり、任意のメソッドまたは不明なメソッドをコールしたりする自由度が高くなります。 これに対応して、コンパイラでは、finalフィールドの値をレジスタにキャッシュし、final以外のフィールドをリロードする必要がある場合にメモリーからリロードしないようにできます。
また、finalフィールドを使用すると、プログラマはスレッドセーフな不変オブジェクトを同期せずに実装できます。 スレッドセーフな不変オブジェクトは、スレッド間で不変オブジェクトへの参照を渡すためにデータ競合が使用されている場合でも、すべてのスレッドで不変と見なされます。 これにより、不変または悪意のあるコードによる不変クラスの誤用に対して安全を保証できます。不変性を保証するには、finalフィールドを正しく使用する必要があります。
オブジェクトは、コンストラクタが終了すると完全に初期化されます。 オブジェクトが完全に初期化された後にのみオブジェクトへの参照を表示できるスレッドは、そのオブジェクトのfinalフィールドに正しく初期化されている値を確認することが保証されます。
finalフィールドの使用モデルは単純なモデルです。そのオブジェクトのコンストラクタでオブジェクトのfinalフィールドを設定し、オブジェクトのコンストラクタが終了する前に別のスレッドが参照できる場所で構築されるオブジェクトへの参照を記述しないでください。 これに従うと、オブジェクトが別のスレッドによって検出されると、そのスレッドには常に、そのオブジェクトのfinalフィールドの正しく構築されたバージョンが表示されます。 また、これらのfinalフィールドによって参照されるオブジェクトまたは配列のバージョンが、finalフィールドと同じくらい最新であることが確認されます。
例17.5-1. Javaメモリー・モデルのfinalフィールド
次のプログラムは、finalフィールドと通常のフィールドとの比較を示しています。
class FinalFieldExample {
final int x;
int y;
static FinalFieldExample f;
public FinalFieldExample() {
x = 3;
y = 4;
}
static void writer() {
f = new FinalFieldExample();
}
static void reader() {
if (f != null) {
int i = f.x; // guaranteed to see 3
int j = f.y; // could see 0
}
}
}
クラスFinalFieldExampleには、final intフィールドxと、final以外のintフィールドyがあります。 あるスレッドがメソッドwriterを実行し、別のスレッドがメソッドreaderを実行する場合があります。
writerメソッドは、オブジェクトのコンストラクタが終了した後にfを書き込むため、readerメソッドは、f.xに対して適切に初期化された値を確認することが保証されます。つまり、値3が読み取られます。 ただし、f.yはfinalではないため、readerメソッドは値4を参照することは保証されません。
例17.5-2.セキュリティのfinalフィールド
finalフィールドは、必要なセキュリティを保証できるように設計されています。 次のプログラムについて考えてみます。 1つのスレッド(スレッド1として参照)が実行されます。
Global.s = "/tmp/usr".substring(4);
別のスレッド(スレッド2)が実行されている間
String myS = Global.s;
if (myS.equals("/tmp"))System.out.println(myS);
Stringオブジェクトは不変であり、文字列操作は同期を実行しません。 String実装にはデータの競合はありませんが、他のコードではStringオブジェクトの使用に関連するデータの競合が発生する可能性があり、メモリー・モデルではデータの競合があるプログラムに対して弱い保証が行われます。 特に、Stringクラスのフィールドがfinalでない場合、スレッド2が最初に文字列オブジェクトのオフセットに対する0のデフォルト値を表示でき、/tmpと等しい値と比較できます(ほとんどありません)。 Stringオブジェクトに対する後の操作では、4の正しいオフセットが表示されるため、Stringオブジェクトは/usrと認識されます。 Javaプログラミング言語のセキュリティ機能の多くは、悪意のあるコードがスレッド間でString参照を渡すためにデータの競合を使用している場合でも、Stringオブジェクトが本当に不変であると認識されることに依存します。
finalフィールドのセマンティクス
oをオブジェクトとし、cを、finalフィールドfが書き込まれるoのコンストラクタにします。 cが正常にまたは突然終了すると、oのfinalフィールドfに対するfreezeアクションが実行されます。
あるコンストラクタが別のコンストラクタを起動し、呼び出されたコンストラクタがfinalフィールドを設定すると、呼び出されたコンストラクタの最後にfinalフィールドのフリーズが発生することに注意してください。
実行ごとに、読取りの動作は、2つの追加部分順序(間接参照チェーンdereferences()とメモリー・チェーンmc())の影響を受けます。これらは、実行の一部とみなされます(したがって、特定の実行に対して固定されます)。 これらの部分オーダーは、次の制約を満たす必要があります(一意のソリューションを使用する必要はありません)。
間接参照チェーン: アクションaが、oを初期化しなかったスレッドtによるオブジェクトoのフィールドまたは要素の読取りまたは書込みである場合、スレッドtによってoのアドレスを参照する読取りrdereferences(r、 a)が存在する必要があります。
メモリー・チェーン: メモリー・チェーンの順序にはいくつかの制約があります。
rが書込みwを参照する読取りの場合は、mc(w、 r)に該当する必要があります。
rおよびaがdereferences(r、 a)のようなアクションである場合は、mc(r、 a)のようにする必要があります。
wが、oを初期化しなかったスレッドtによるオブジェクトoのアドレスの書込みである場合、スレッドtによる読取りrが存在し、mc(r、 w)のアドレスがmc(r、 w)になるようにする必要があります。
書き込み w、フリーズ f、アクション a (finalフィールドの読み取りではない)、fによってフリーズされた finalフィールドの r1、および読み取り r2が hb()w、f)、hb(f、a)、mc(a、r1)、および dereferences(r1、r2)で、r2で確認できる値を判断する際には、hb(w、 r2)について検討します。 (このhappens-before順序付けは、他のhappens-before順序付けと遷移的にクローズしません。)
dereferencesの順序は反射的であり、r1は r2と同じにすることができます。
finalフィールドの読取りの場合、finalフィールドの読取り前に存在するとみなされる書込みは、finalフィールド・セマンティクスによって導出された書込みのみです。
finalフィールドの読取り
通常のhappens-beforeルールによるコンストラクタ内のそのフィールドの初期化に関して、そのオブジェクトを構成するスレッド内のオブジェクトのfinalフィールドの読取り。 コンストラクタでフィールドが設定された後に読取りが発生すると、finalフィールドが割り当てられた値が表示され、それ以外の場合はデフォルト値が表示されます。
finalフィールドの以降の変更
デシリアライズなど、場合によっては、構築後にオブジェクトのfinalフィールドを変更する必要があります。finalフィールドは、リフレクションやその他の実装に依存する手段によって変更できます。 これが妥当なセマンティクスを持つ唯一のパターンは、オブジェクトが構築されてから、オブジェクトのfinalフィールドが更新されるパターンです。 このオブジェクトは、オブジェクトのfinalフィールドに対するすべての更新が完了するまで、他のスレッドから参照できるようにしたり、finalフィールドを読み取ることはできません。 finalフィールドのフリーズは、finalフィールドが設定されているコンストラクタの末尾と、リフレクションまたはその他の特別なメカニズムによるfinalフィールドの変更の直後の両方で発生します。
それでも、いくつかの合併症があります。 finalフィールドがフィールド宣言で定数式(§15.29)に初期化されている場合、finalフィールドへの変更は監視されないことがあります。これは、そのfinalフィールドの使用は、コンパイル時に定数式の値に置き換えられるためです。
もう1つの問題は、仕様でfinalフィールドを積極的に最適化できることです。 スレッド内では、コンストラクタ内で行われないfinalフィールドの変更を使用して、finalフィールドの読取りを並べ替えることができます。
例17.5.3-1. finalフィールドの積極的な最適化
class A {
final int x;
A() {
x = 1;
}
int f() {
return d(this,this);
}
int d(A a1, A a2) {
int i = a1.x;
g(a1);
int j = a2.x;
return j - i;
}
static void g(A a) {
// uses reflection to change a.x to 2
}
}
dメソッドでは、コンパイラはxの読取りとgの呼出しを自由に並べ替えることができます。 したがって、new A().f()は-1、0または1を返すことができます。
実装は、final-field-safeコンテキストでコードのブロックを実行する方法を提供できます。 オブジェクトがfinal-field-safeコンテキスト内に構築されている場合、そのオブジェクトのfinalフィールドの読取りは、そのfinal-field-safeコンテキスト内で発生するfinalフィールドの変更によって並べ替えられません。
finalフィールド・セーフ・コンテキストには、追加の保護があります。 スレッドがfinalフィールドのデフォルト値を参照できるオブジェクトへの参照を誤って公開し、finalフィールド・セーフ・コンテキスト内でオブジェクトへの適切な公開済参照を読み取ると、finalフィールドの正しい値を確認することが保証されます。 形式主義では、finalフィールド・セーフ・コンテキスト内で実行されるコードは、個別のスレッドとして扱われます(finalフィールド・セマンティクスの目的のみ)。
実装では、コンパイラはfinalフィールドへのアクセスをfinalフィールド・セーフ・コンテキストの内外に移動しないでください(ただし、そのコンテキスト内でオブジェクトが構築されていないかぎり、そのようなコンテキストの実行を中心に移動できます)。
finalフィールド・セーフ・コンテキストの使用が適している場所の1つは、エグゼキュータまたはスレッド・プールにあります。 エグゼキュータは、各Runnableを個別のfinalフィールド・セーフ・コンテキストで実行することで、1つのRunnableによるオブジェクトoへの不正なアクセスによって、同じエグゼキュータによって処理される他のRunnableのfinalフィールドが保証されないことを保証できます。
通常、finalおよびstaticのフィールドは変更できません。 ただし、System.in、System.outおよびSystem.errはstatic finalフィールドであり、従来の理由から、メソッドSystem.setIn、System.setOutおよびSystem.setErrによる変更を許可する必要があります。 これらのフィールドを、通常のfinalフィールドと区別するために書込み保護と呼びます。
コンパイラは、これらのフィールドを他のfinalフィールドとは異なる方法で処理する必要があります。 たとえば、通常のfinalフィールドの読取りは同期の「免疫」です。ロックまたは揮発性読取りに関係するバリアは、finalフィールドから読み取られる値に影響を与えません。 書き込み保護されたフィールドの値は変更される可能性があるため、同期イベントはそれらに影響を与えます。 したがって、セマンティクスでは、これらのフィールドは、ユーザー・コードがSystemクラス内にないかぎり、ユーザー・コードで変更できない通常のフィールドとして扱われることを規定しています。
Java Virtual Machineの実装に関する考慮事項の1つは、すべてのフィールドおよび配列要素が個別とみなされることです。1つのフィールドまたは要素に対する更新は、他のフィールドまたは要素の読取りまたは更新と相互作用してはなりません。 特に、バイト配列の隣接する要素を個別に更新する2つのスレッドは、干渉したり、相互作用したりしてはならず、順次整合性を確保するために同期を必要としません。
プロセッサの中には、1バイトに書き込めないものがあります。 単語全体を読み取り、適切なバイトを更新し、単語全体をメモリーに書き戻すだけで、このようなプロセッサにバイト配列の更新を実装することは違法です。 この問題はワード引き裂きと呼ばれることがあり、シングル・バイトを単独で簡単に更新できないプロセッサでは、他のアプローチが必要になります。
例17.6-1. Word Tearingの検出
次のプログラムは、単語の引き裂きを検出するためのテストケースです。
public class WordTearing extends Thread {
static final int LENGTH = 8;
static final int ITERS = 1000000;
static byte[] counts = new byte[LENGTH];
static Thread[] threads = new Thread[LENGTH];
final int id;
WordTearing(int i) {
id = i;
}
public void run() {
byte v = 0;
for (int i = 0; i < ITERS; i++) {
byte v2 = counts[id];
if (v != v2) {
System.err.println("Word-Tearing found: " +
"counts[" + id + "] = "+ v2 +
", should be " + v);
return;
}
v++;
counts[id] = v;
}
}
public static void main(String[] args) {
for (int i = 0; i < LENGTH; ++i)
(threads[i] = new WordTearing(i)).start();
}
}
これにより、隣接するバイトへの書き込みによってバイトを上書きできないようになります。
doubleおよびlongの非アトミック処理 Javaプログラミング言語のメモリー・モデルでは、不揮発性longまたはdouble値への単一の書込みは、32ビット・ハーフごとに1つずつ、2つの個別の書込みとして処理されます。 これにより、ある書き込みから64ビット値の最初の32ビットがスレッドで認識され、別の書き込みから2番目の32ビットがスレッドで認識される状況が発生する可能性があります。
揮発性longおよびdouble値の書込みおよび読取りは、常にアトミックです。
参照への書込みと読取りは、32ビット値または64ビット値として実装されているかどうかに関係なく、常にアトミックです。
一部の実装では、64ビットのlongまたはdouble値に対する単一の書込みアクションを、隣接する32ビット値に対する2つの書込みアクションに分割すると便利です。 効率化のために、この動作は実装固有です。Java Virtual Machineの実装では、longおよびdouble値への書込みをアトミックまたは2つの部分で自由に実行できます。
可能な場合は64ビット値を分割しないように、Java Virtual Machineの実装が推奨されます。 プログラマは、64ビットの共有値をvolatileとして宣言するか、プログラムを正しく同期して、複雑な問題を回避することをお薦めします。