スレッドセーフティには2つの側面があることを理解することが重要です。
- 実行制御、および
- メモリの可視性
1つ目は、コードが実行されるタイミング(命令が実行される順序を含む)とそれが同時に実行できるかどうかを制御することであり、2つ目は、メモリ内で行われた処理の効果が他のスレッドから見えるようにする場合です。各CPUはメインメモリとの間にいくつかのレベルのキャッシュを持っているため、スレッドがメインメモリのプライベートコピーを取得して処理することが許可されているため、異なるCPUまたはコアで実行されているスレッドは、いつでも異なる方法で「メモリ」を参照できます。
を使用synchronized
すると、他のスレッドが同じオブジェクトのモニター(またはロック)を取得できなくなり、同じオブジェクトの同期によって保護されているすべてのコードブロックが同時に実行されなくなります。同期は、「前に起こる」メモリバリアも作成します。これにより、一部のスレッドがロックを解放するまでに行われた処理は別のスレッドに表示され、その後、同じロックを取得してから、ロックを取得する前に発生しました。実際には、現在のハードウェアでは、これにより通常、モニターが取得されるとCPUキャッシュがフラッシュされ、解放されるとメインメモリに書き込まれます。どちらも(比較的)高価です。
volatile
一方、を使用すると、揮発性変数へのすべてのアクセス(読み取りまたは書き込み)が強制的にメインメモリに発生し、揮発性変数をCPUキャッシュから効果的に除外できます。これは、変数の可視性が正しいことが必要で、アクセスの順序が重要ではない一部のアクションに役立ちます。使い方volatile
もの治療を変更long
し、double
アトミックであるそれらへのアクセスを必要とします。一部の(古い)ハードウェアでは、ロックが必要になる場合がありますが、最新の64ビットハードウェアでは必要ありません。Java 5+用の新しい(JSR-133)メモリモデルでは、揮発性のセマンティクスが強化され、メモリの可視性と命令の順序に関して同期とほぼ同じ強さになります(http://www.cs.umd.eduを参照) /users/pugh/java/memoryModel/jsr-133-faq.html#volatile)。可視化のために、揮発性フィールドへの各アクセスは、同期の半分のように機能します。
新しいメモリモデルでは、揮発性変数を相互に並べ替えることができないのは事実です。違いは、通常のフィールドアクセスを並べ替えるのがそれほど簡単ではなくなったことです。揮発性フィールドへの書き込みはモニターリリースと同じメモリー効果を持ち、揮発性フィールドからの読み取りはモニター取得と同じメモリー効果を持ちます。実際、新しいメモリモデルでは、揮発性フィールドアクセスと他のフィールドアクセス(揮発性かどうかに関係なく)の並べ替えに厳しい制約が課されるため、揮発性フィールドへのA
書き込み時にスレッドから見えるものは、読み取り時f
にスレッドから見えるようB
になりますf
。
- JSR 133(Javaのメモリモデル)よくある質問
したがって、現在両方の形式のメモリバリア(現在の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が再開し、counterを0に設定します。その後、スレッド2が再開し、counterを0に設定します。これは、スレッド1からの更新を逃したためです。これは、前述のようにスレッドの切り替えが発生しなかった場合でも発生する可能性がありますが、これは、2つの異なるCPUコアにカウンターの2つの異なるキャッシュコピーが存在し、スレッドがそれぞれ別のコアで実行されたためです。さらに言えば、キャッシングのために、1つのスレッドが1つの値でカウンターを持ち、もう1つのスレッドがまったく異なる値でカウンターを持つことができます。
この例で重要なのは、変数カウンターがメインメモリからキャッシュに読み込まれ、キャッシュで更新され、後でメモリバリアが発生したとき、またはキャッシュメモリが他の何かのために必要になったときの不確定ポイントでのみメインメモリに書き戻されたことです。volatile
最大値のテストと割り当ては、非アトミックなread+increment+write
機械命令のセットであるインクリメントを含む個別の操作であるため、カウンターを作成することはこのコードのスレッドセーフには不十分です。
MOV EAX,counter
INC EAX
MOV counter,EAX
揮発性変数は、それらに対して実行されるすべての操作が「アトミック」である場合にのみ役立ちます。たとえば、完全に形成されたオブジェクトへの参照が読み取りまたは書き込みのみの場合(実際、通常は1つのポイントからのみ書き込まれる場合)です。別の例は、配列が最初に参照のローカルコピーを取得することによってのみ読み取られた場合、コピーオンライトリストをサポートする揮発性配列参照です。