スレッド セーフには2 つの側面があることを理解することが重要です。
- 実行制御、および
- メモリの可視性
1 つ目は、コードがいつ実行されるか (命令が実行される順序を含む) を制御し、同時に実行できるかどうかに関係し、2 つ目は、実行された内容のメモリ内の効果が他のスレッドに表示されるタイミングに関係します。各 CPU とメイン メモリの間にいくつかのレベルのキャッシュがあるため、スレッドはメイン メモリのプライベート コピーを取得して操作することが許可されているため、異なる CPU またはコアで実行されているスレッドは、任意の時点で異なる方法で「メモリ」を認識できます。
を使用synchronized
すると、他のスレッドが同じオブジェクトのモニター (またはロック)を取得できなくなり、同じオブジェクトの同期によって保護されているすべてのコード ブロックが同時に実行されなくなります。同期はまた、「前に発生する」メモリ バリアを作成し、メモリの可視性制約を引き起こします。たとえば、あるスレッドがロックを解放するまでに実行されたすべての処理が、その後同じロックを取得した別のスレッドには、ロックを取得する前に発生したように見えます。実際には、現在のハードウェアでは、これにより通常、モニターが取得されたときに CPU キャッシュがフラッシュされ、モニターが解放されたときにメイン メモリに書き込まれます。どちらも (比較的) コストがかかります。
一方、を使用volatile
すると、volatile 変数へのすべてのアクセス (読み取りまたは書き込み) が強制的にメイン メモリに発生し、volatile 変数が CPU キャッシュから効果的に除外されます。これは、変数の可視性が正しく、アクセスの順序が重要ではないことが単に必要な一部のアクションに役立ちます。を使用すると、とvolatile
の処理も変更され、それらへのアクセスがアトミックである必要があります。一部の (古い) ハードウェアではロックが必要になる場合がありますが、最新の 64 ビット ハードウェアでは必要ありません。Java 5+ の新しい (JSR-133) メモリ モデルでは、volatile のセマンティクスが強化され、メモリの可視性と命令の順序に関して同期とほぼ同じくらい強力になりました ( http://www.cs.umd.eduを参照)。 /users/pugh/java/memoryModel/jsr-133-faq.html#volatilelong
double
)。可視性のために、揮発性フィールドへの各アクセスは半分の同期のように機能します。
新しいメモリ モデルでは、volatile 変数を相互に並べ替えることができないことは依然として事実です。違いは、それらの周りの通常のフィールド アクセスを並べ替えるのがそれほど簡単ではなくなったことです。揮発性フィールドへの書き込みには、モニターのリリースと同じメモリー効果があり、揮発性フィールドからの読み取りには、モニターの取得と同じメモリー効果があります。実際には、新しいメモリ モデルでは、volatile フィールドへのアクセスと他のフィールド アクセス (volatile であるかどうかに関係なく) の並べ替えに対してより厳しい制約が課されるため、 volatile フィールドへのA
書き込み時にスレッドに表示されていたものはすべて、読み取り時f
にスレッドに表示されます。B
f
-- JSR 133 (Java メモリ モデル) FAQ
そのため、(現在の JMM では) メモリ バリアの両方の形式が命令の並べ替えバリアを引き起こし、コンパイラまたはランタイムがバリアを越えて命令を並べ替えることができなくなります。古い JMM では、volatile は並べ替えを妨げませんでした。これは重要な場合があります。メモリ バリアを除けば、課される唯一の制限は、 特定のスレッドに対して、コードの最終的な効果が、命令がスレッドに表示される順序で正確に実行された場合と同じになるということだからです。ソース。
volatile の 1 つの用途は、共有されているが不変のオブジェクトがオンザフライで再作成され、他の多くのスレッドが実行サイクルの特定の時点でオブジェクトへの参照を取得することです。再作成されたオブジェクトが公開されたら、他のスレッドがそのオブジェクトの使用を開始する必要がありますが、完全な同期による追加のオーバーヘッドは必要なく、それに付随する競合とキャッシュのフラッシュが発生します。
// Declaration
public class SharedLocation {
static public SomeObject someObject=new SomeObject(); // default object
}
// Publishing code
// Note: do not simply use SharedLocation.someObject.xxx(), since although
// someObject will be internally consistent for xxx(), a subsequent
// call to yyy() might be inconsistent with xxx() if the object was
// replaced in between calls.
SharedLocation.someObject=new SomeObject(...); // new object is published
// Using code
private String getError() {
SomeObject myCopy=SharedLocation.someObject; // gets current copy
...
int cod=myCopy.getErrorCode();
String txt=myCopy.getErrorText();
return (cod+" - "+txt);
}
// And so on, with myCopy always in a consistent state within and across calls
// Eventually we will return to the code that gets the current SomeObject.
具体的には、読み取り、更新、書き込みの質問について話します。次の安全でないコードを検討してください。
public void updateCounter() {
if(counter==1000) { counter=0; }
else { counter++; }
}
現在、 updateCounter() メソッドが同期されていないため、2 つのスレッドが同時にこのメソッドに入る可能性があります。発生する可能性のある多くの順列の 1 つは、スレッド 1 が counter==1000 のテストを実行し、それが true であることを検出してから中断されることです。次に、スレッド 2 が同じテストを実行し、それが true であることを確認して中断されます。次に、スレッド 1 が再開し、カウンターを 0 に設定します。次に、スレッド 2 が再開し、スレッド 1 からの更新に失敗したため、カウンターを再び 0 に設定します。これは、説明したようにスレッドの切り替えが発生しない場合でも発生する可能性があります。これは、2 つの異なるキャッシュされたカウンターのコピーが 2 つの異なる CPU コアに存在し、スレッドがそれぞれ別のコアで実行されたためです。さらに言えば、1 つのスレッドが 1 つの値でカウンターを持ち、もう 1 つのスレッドがキャッシュのためにまったく異なる値でカウンターを持つ可能性があります。
この例で重要なことは、変数counterがメイン メモリからキャッシュに読み込まれ、キャッシュで更新され、後でメモリ バリアが発生したとき、またはキャッシュ メモリが別の目的で必要になったときに、不確定な時点でのみメイン メモリに書き戻されたことです。このコードのスレッド セーフには、カウンターを作成するだけでは不十分です。これは、最大値のテストと割り当てが、次のようなvolatile
非アトミック マシン命令のセットであるインクリメントを含む個別の操作であるためです。read+increment+write
MOV EAX,counter
INC EAX
MOV counter,EAX
揮発性変数は、それらに対して実行されるすべての操作が「アトミック」である場合にのみ役立ちます。たとえば、完全に形成されたオブジェクトへの参照が読み取りまたは書き込みのみである私の例などです (実際、通常は単一のポイントからのみ書き込まれます)。もう 1 つの例は、コピー オン ライト リストをサポートする揮発性配列参照です。ただし、最初に配列への参照のローカル コピーを取得することによってのみ配列が読み取られます。