変数をvolatile
として宣言することと、Javaのsynchronized(this)
ブロック内の変数に常にアクセスすることの違いを疑問に思っていますか?
この記事によると http://www.javamex.com/tutorials/synchronization_volatile.shtml 言わなければならないことがたくさんあり、多くの違いがありますが、いくつかの類似点もあります。
私は特にこの情報に興味があります:
...
- volatile変数へのアクセスがブロックされる可能性はありません。単純な読み取りまたは書き込みのみを行うため、同期ブロックとは異なり、ロックを保持することはありません。
- 揮発性変数にアクセスすることは決してロックを保持しないため、アトミック操作としてread-update-writeしたい場合には適していません(ただし、 「更新を逃す」準備ができている);
read-update-writeとはどういう意味ですか?書き込みも更新ではありませんか、それとも単にupdateが読み取りに依存する書き込みであることを意味しますか?
何よりも、volatile
ブロックを介して変数にアクセスするよりも、変数synchronized
を宣言する方が適しているのはいつですか?入力に依存する変数にvolatile
を使用することをお勧めしますか?たとえば、render
という変数があり、レンダリングループを介して読み取られ、keypressイベントによって設定されますか?
スレッドセーフにはtwoの側面があることを理解することが重要です。
1つ目は、コードが実行されるタイミング(命令の実行順序を含む)と、同時に実行できるかどうかの制御に関係し、2つ目は、実行された内容のメモリ内の効果が他のスレッドに見える場合に関係します。各CPUにはメインメモリとの間に複数のレベルのキャッシュがあるため、スレッドはメインメモリのプライベートコピーを取得して作業することが許可されているため、異なるCPUまたはコアで実行されるスレッドは、任意の時点で異なる方法で「メモリ」を見ることができます。
synchronized
を使用すると、他のスレッドがモニター(またはロック)を取得できなくなります同じオブジェクトにより、同期によって保護されたすべてのコードブロックが防止されます- 同じオブジェクトで同時実行から。同期alsoは、「発生前」のメモリバリアを作成し、一部のスレッドがロックを解放するまで行われたものが表示される続いて別のスレッドが同じロックを取得してから、ロックを取得する前に発生します。実際的には、現在のハードウェアでは、これにより通常、モニターが取得されるとCPUキャッシュがフラッシュされ、リリースされるとメインメモリに書き込まれます。どちらも(比較的)高価です。
一方、volatile
を使用すると、揮発性変数へのすべてのアクセス(読み取りまたは書き込み)がメインメモリに強制的に行われ、揮発性変数がCPUキャッシュから事実上保持されます。これは、変数の可視性が正しいことが単に必要であり、アクセスの順序が重要ではない一部のアクションに役立ちます。 volatile
を使用すると、long
およびdouble
の処理も変更され、それらへのアクセスがアトミックであることが要求されます。一部の(古い)ハードウェアではロックが必要になる場合がありますが、最新の64ビットハードウェアではそうではありません。 Java 5+の新しい(JSR-133)メモリモデルでは、volatileのセマンティクスが強化され、メモリの可視性と命令の順序に関して同期とほぼ同じ強度になりました( http ://www.cs.umd.edu/users/pugh/Java/memoryModel/jsr-133-faq.html#volatile )。可視性のために、volatileフィールドへの各アクセスは、半分の同期のように機能します。
新しいメモリモデルでは、揮発性変数を相互に並べ替えることはできません。違いは、それらの周囲の通常のフィールドアクセスを並べ替えるのが簡単ではなくなったことです。揮発性フィールドへの書き込みは、モニターリリースと同じメモリー効果を持ち、揮発性フィールドからの読み取りは、モニター取得と同じメモリー効果を持ちます。実際、新しいメモリモデルは、揮発性フィールドアクセスを他のフィールドアクセス(揮発性または非揮発性)と並べ替える際により厳しい制約を課すため、スレッド
A
が揮発性フィールドf
に書き込むときに可視であったものはすべて可視になりますB
を読み取るときにf
をスレッド化します。
したがって、現在の両方の形式のメモリバリア(現在の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つは、thread-1がcounter == 1000のテストを実行し、それがtrueであると判断してから中断されるということです。次に、スレッド2は同じテストを実行し、それが真であると判断して中断します。次に、スレッド1が再開し、カウンターを0に設定します。次に、スレッド2が再開し、スレッド1からの更新を見逃したため、カウンターを再び0に設定します。これは、説明したようにスレッドの切り替えが発生しなくても、2つの異なるCPUコアにカウンターの2つの異なるキャッシュコピーが存在し、スレッドがそれぞれ別のコアで実行された場合でも発生します。さらに言えば、1つのスレッドが1つの値のカウンターを持ち、もう1つのスレッドがキャッシュのためにまったく異なる値のカウンターを持つことができます。
この例で重要なのは、変数counterがメインメモリからキャッシュに読み込まれ、キャッシュで更新され、その後メモリバリアが発生したときまたはメモリバリアが発生したときの不確定な時点でのみメインメモリに書き戻されたことですキャッシュメモリは他の何かのために必要でした。カウンターvolatile
の作成は、このコードのスレッドセーフには不十分です。これは、最大値と割り当てのテストが、非アトミックread+increment+write
マシン命令のセットであるインクリメントを含む個別の操作であるためです。何かのようなもの:
MOV EAX,counter
INC EAX
MOV counter,EAX
all変数に対して実行される操作が「アトミック」である場合にのみ、揮発性変数は有用です。たとえば、完全に形成されたオブジェクトへの参照は読み取りまたは書き込みのみです。 (そして実際、通常は単一のポイントからのみ書かれています)。別の例は、コピーオンライトリストをバッキングする揮発性配列参照です。ただし、最初に参照のローカルコピーを取得することによってのみ配列が読み取られた場合に限ります。
volatileはfield modifierですが、synchronizedはcode blocksおよびmethodsを変更します。したがって、これら2つのキーワードを使用して、単純なアクセサーの3つのバリエーションを指定できます。
int i1; int geti1() {return i1;} volatile int i2; int geti2() {return i2;} int i3; synchronized int geti3() {return i3;}
geti1()
は、現在のスレッドのi1
に現在保存されている値にアクセスします。スレッドは変数のローカルコピーを持つことができ、データは他のスレッドに保持されているデータと同じである必要はありません。特に、別のスレッドがそのスレッドのi1
を更新したかもしれませんが、現在のスレッドの値はその更新された値。実際、Javaには「メイン」メモリの概念があり、これは変数の現在の「正しい」値を保持するメモリです。スレッドは変数のデータの独自のコピーを持つことができ、スレッドのコピーは「メイン」メモリとは異なる場合があります。そのため、実際には、「メイン」メモリの値が1のi1
、スレッド1の値が2のi1
、および-の可能性があります。 thread2thread1とthread2の両方がi1を更新したが、それらの更新された値がまだない場合、i1
の値はになります「メイン」メモリまたは他のスレッドに伝播された。一方、
geti2()
は、「メイン」メモリからi2
の値に効果的にアクセスします。揮発性変数は、「メイン」メモリに現在保持されている値とは異なる変数のローカルコピーを持つことはできません。事実上、volatileとして宣言された変数は、すべてのスレッドで同期されたデータを持っている必要があります。そのため、任意のスレッドで変数にアクセスまたは更新すると、他のすべてのスレッドはすぐに同じ値を参照します。一般に、揮発性変数は、「プレーン」変数よりもアクセスおよび更新のオーバーヘッドが高くなります。一般に、スレッドは効率を上げるために独自のデータのコピーを持つことが許可されています。揮発性と同期には2つの違いがあります。
最初に同期は、一度に1つのスレッドのみを強制的にコードブロックを実行できるモニターのロックを取得および解放します。それは、同期するかなりよく知られた側面です。しかし、同期はメモリも同期します。実際、synchronizedはスレッドメモリ全体を「メイン」メモリと同期します。したがって、
geti3()
を実行すると次のことが行われます。
- スレッドは、オブジェクトthisのモニターのロックを取得します。
- スレッドメモリはすべての変数をフラッシュします。つまり、すべての変数が「メイン」メモリから効果的に読み取られます。
- コードブロックが実行されます(この場合、戻り値をi3の現在の値に設定します。これは「メイン」メモリからリセットされたばかりの場合があります)。
- (変数への変更は通常「メイン」メモリに書き出されますが、geti3()には変更はありません。)
- スレッドは、オブジェクトthisのモニターのロックを解除します。
したがって、volatileがスレッドメモリと「メイン」メモリ間で1つの変数の値のみを同期する場合、同期はスレッドメモリと「メイン」メモリ間ですべての変数の値を同期し、起動するモニタをロックおよびリリースします。明らかに同期は、揮発性よりもオーバーヘッドが大きい可能性があります。
http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html
synchronized
は、メソッドレベル/ブロックレベルのアクセス制限修飾子です。 1つのスレッドがクリティカルセクションのロックを所有していることを確認します。ロックを所有するスレッドのみがsynchronized
ブロックに入ることができます。他のスレッドがこのクリティカルセクションにアクセスしようとする場合、現在の所有者がロックを解除するまで待機する必要があります。
volatile
は、すべてのスレッドがメインメモリから変数の最新の値を取得することを強制する変数アクセス修飾子です。 volatile
変数にアクセスするためにロックは必要ありません。すべてのスレッドは、同時にvolatile変数値にアクセスできます。
Volatile変数を使用する良い例:Date
変数。
日付変数volatile
を作成したと仮定します。この変数にアクセスするすべてのスレッドは常にメインメモリから最新のデータを取得するため、すべてのスレッドは実際の(実際の)日付値を示します。同じ変数に対して異なる時間を示す異なるスレッドは必要ありません。すべてのスレッドに正しい日付値が表示されます。
volatile
コンセプトの理解を深めるために、これをご覧ください 記事 .
ローレンス・ドルはあなたのread-write-update query
を明確に説明しました。
他のクエリについて
Synchronizedを介して変数にアクセスするよりも、変数をvolatileとして宣言する方が適しているのはいつですか?
Date変数について説明した例のように、すべてのスレッドがリアルタイムで変数の実際の値を取得する必要があると思う場合は、volatile
を使用する必要があります。
入力に依存する変数にvolatileを使用することをお勧めしますか?
回答は最初のクエリと同じです。
これを参照してください 記事 理解を深めてください。
jenkovの の説明が好きです。マルチスレッド環境。
共有オブジェクトの可視性
volatile宣言または同期を適切に使用せずに2つ以上のスレッドがオブジェクトを共有している場合、1つのスレッドによって行われた共有オブジェクトの更新は、他のスレッドから見えない場合があります。
共有オブジェクトが最初にメインメモリに保存されると想像してください。 CPU 1で実行されているスレッドは、共有オブジェクトをCPUキャッシュに読み込みます。そこで共有オブジェクトに変更を加えます。 CPUキャッシュがメインメモリにフラッシュバックされていない限り、変更されたバージョンの共有オブジェクトは、他のCPUで実行されているスレッドには表示されません。この方法では、各スレッドは共有オブジェクトの独自のコピーで終わる可能性があり、各コピーは異なるCPUキャッシュに置かれます。
次の図は、スケッチされた状況を示しています。左側のCPUで実行されている1つのスレッドは、共有オブジェクトをCPUキャッシュにコピーし、changesそのcount変数を2に変更します。この変更は、実行中の他のスレッドには表示されませんカウントする更新がまだメインメモリにフラッシュバックされていないため、適切なCPU。
この問題を解決するには、 Javaのvolatileキーワード を使用できます。 volatile keyword
[約] は、指定された変数が読み取りから直接メインメモリになり、更新時に常に書き込みからメインメモリに戻ることを確認できます。
レース条件
2つ以上のスレッドがオブジェクトを共有し、複数のスレッドがその共有オブジェクトの変数を更新する場合、race conditions
が発生する場合があります。
thread A
が共有オブジェクトの変数カウントをCPUキャッシュに読み込むと想像してください。 thread B
も同じことを行いますが、異なるCPUキャッシュに入れてください。 thread A
はカウントに1を追加し、thread B
は同じことを行います。これで、var1が2回、各CPUキャッシュに1つずつ増えました。
これらのインクリメントがシーケンシャル実行された場合、変数カウントは2回インクリメントされ、元のvalue + 2がメインメモリに書き戻されます。
ただし、2つの増分は、適切な同期なしで同時に実行されました。スレッドAとBのどちらがカウントの更新されたバージョンをメインメモリに書き戻すかに関係なく、更新された値は、2つの増分にもかかわらず元の値より1だけ大きくなります。
この図は、上記のような競合状態の問題の発生を示しています。
この問題を解決するには、 Java synchronizedブロック を使用できます。
volatile
で宣言されているかどうかに関係なく、再び。関連トピック 比較とスワップ