Javaでの揮発性と同期の違い


233

変数をJava として宣言することとvolatile、常にsynchronized(this)ブロック内の変数にアクセスすることの違いに疑問を感じていますか?

この記事http://www.javamex.com/tutorials/synchronization_volatile.shtmlによると、言うべきことがたくさんあり、多くの違いがありますが、いくつかの類似点もあります。

私はこの情報に特に興味があります:

...

  • 揮発性変数へのアクセスがブロックする可能性は決してありません。単純な読み取りまたは書き込みを行うだけなので、同期ブロックとは異なり、ロックを保持することはありません。
  • volatile変数にアクセスしてもロックは保持されないため、アトミック操作として読み取り、更新、書き込みを行う場合には適していません(「更新を見逃す」準備ができていない限り)。

read-update-writeとはどういう意味ですか?書き込みも更新ではないのですか、それとも単に更新が読み取りに依存する書き込みであることを意味していますか?

volatileよりも、synchronizedブロックを介して変数にアクセスするよりも、変数を宣言する方が適しているのはいつですか。volatile入力に依存する変数に使用するのは良い考えですか?たとえばrender、レンダリングループを通じて読み取られ、keypressイベントによって設定されるという変数がありますか?

回答:


383

スレッドセーフティには2つの側面があることを理解することが重要です。

  1. 実行制御、および
  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つのポイントからのみ書き込まれる場合)です。別の例は、配列が最初に参照のローカルコピーを取得することによってのみ読み取られた場合、コピーオンライトリストをサポートする揮発性配列参照です。


5
どうもありがとう!カウンターの例は簡単に理解できます。しかし、物事が現実になると、少し異なります。
アルバスダンブルドア2010

「実際には、現在のハードウェアでは、これは通常、モニターが取得されたときにCPUキャッシュのフラッシュを引き起こし、モニターが解放されたときにメインメモリに書き込みます。どちらも(比較的言えば)高価です。」。CPUキャッシュと言うと、各スレッドにローカルなJavaスタックと同じですか?または、スレッドには独自のローカルバージョンのヒープがありますか?私がここでばかげているなら謝罪してください。
NishM

1
@nishm同じではありませんが、関連するスレッドのローカルキャッシュが含まれます。。
Lawrence Dol、2015

1
@MarianPaździoch:インクリメントまたはデクリメントは読み取りで書き込みでもありません。読み取り書き込みです。レジスタへの読み取り、レジスタのインクリメント、そしてメモリへの書き戻しです。読み取りと書き込みは個別にアトミックですが、そのような複数の操作はそうではありません。
Lawrence Dol

2
だから、FAQによると、ない作られただけでアクションロック買収以降は、ロック解除後に可視化されているが、すべてそのスレッドによって行われたアクションが見えるようになります。ロック取得前に行われたアクションも。
Lii

97

volatileフィールド修飾子ですがsynchronizedコードブロックメソッドを修飾します。したがって、次の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には「メイン」メモリという考え方があり、これが変数の現在の「正しい」値を保持するメモリです。スレッドは、変数のデータの独自のコピーを持つことができ、スレッドのコピーは「メイン」メモリとは異なる場合があります。したがって、実際には、「メイン」メモリの値が1i1、thread1の値が2i1thread2の値が2である可能性があります。thread1thread2の両方がi1を更新したが、それらの更新された値が「メイン」メモリまたは他のスレッドにまだ伝播されていない場合は、値3になります。i1

一方、「メイン」メモリのgeti2()値に効率的にアクセスしますi2。揮発性変数は、「メイン」メモリに現在保持されている値とは異なる変数のローカルコピーを持つことはできません。事実上、volatileと宣言された変数は、すべてのスレッド間でデータを同期させる必要があります。これにより、任意のスレッドで変数にアクセスまたは更新すると、他のすべてのスレッドはすぐに同じ値を参照できます。通常、揮発性変数は「プレーン」変数よりもアクセスと更新のオーバーヘッドが高くなります。一般に、スレッドはデータの独自のコピーを持つことが許可されており、効率が向上します。

揮発性と同期の間に2つの違いがあります。

最初に同期されるのは、コードブロックの実行を一度に1つのスレッドのみに強制できるモニターのロックを取得および解放することです。これは、同期化に関してかなりよく知られている側面です。ただし、同期するとメモリも同期します。実際にsynchronizedは、スレッドメモリ全体を「メイン」メモリと同期させます。したがって、実行するgeti3()と次のようになります。

  1. スレッドは、オブジェクトthisのモニターのロックを取得します。
  2. スレッドメモリはすべての変数をフラッシュします。つまり、スレッドメモリはすべての変数を「メイン」メモリから効果的に読み取ります。
  3. コードブロックが実行されます(この場合、戻り値をi3の現在の値に設定します。これは、「メイン」メモリからリセットされたばかりの可能性があります)。
  4. (変数への変更は通常、「メイン」メモリに書き出されますが、geti3()の場合、変更はありません。)
  5. スレッドはオブジェクトthisのモニターのロックを解放します。

したがって、volatileはスレッドメモリと「メイン」メモリ間で1つの変数の値のみを同期しますが、synchronizedはスレッドメモリと「メイン」メモリ間ですべての変数の値を同期し、モニターをロックして解放して起動します。明らかに同期されていると、揮発性よりもオーバーヘッドが多くなる可能性があります。

http://javaexp.blogspot.com/2007/12/difference-between-volatile-and.html


35
-1、Volatileはロックを取得しません。基盤となるCPUアーキテクチャを使用して、書き込み後のすべてのスレッドの可視性を保証します。
Michael Barker

書き込みの原子性を保証するためにロックが使用される場合があることに注意してください。たとえば、拡張された幅の権利をサポートしない32ビットプラットフォームでlongを記述します。Intelは、SSE2レジスタ(128ビット幅)を使用して揮発性のlongを処理することにより、これを回避します。ただし、volatileをロックと見なすと、コードに厄介なバグが発生する可能性があります。
Michael Barker

2
揮発性変数のロックによって共有される重要なセマンティクスは、どちらもHappens-Beforeエッジを提供することです(Java 1.5以降)。同期ブロックの入力、ロックの取得、揮発性からの読み取りはすべて「取得」と見なされ、ロックの解放、同期ブロックの終了、揮発性の書き込みはすべて「解放」の形式です。
マイケルバーカー

20

synchronizedメソッドレベル/ブロックレベルのアクセス制限修飾子です。1つのスレッドがクリティカルセクションのロックを所有していることを確認します。ロックを所有するスレッドのみがsynchronizedブロックに入ることができます。他のスレッドがこのクリティカルセクションにアクセスしようとしている場合、現在の所有者がロックを解放するまで待機する必要があります。

volatileすべてのスレッドがメインメモリから変数の最新の値を取得するように強制する変数アクセス修飾子です。volatile変数にアクセスするためにロックは必要ありません。すべてのスレッドは、揮発性変数の値に同時にアクセスできます。

volatile変数を使用する良い例:Date変数。

Date変数を作成したと仮定しますvolatile。この変数にアクセスするすべてのスレッドは常にメインメモリから最新のデータを取得するため、すべてのスレッドは実際の(実際の)日付値を示します。同じ変数に対して異なるスレッドを表示する必要はありません。すべてのスレッドは正しい日付値を表示する必要があります。

ここに画像の説明を入力してください

コンセプトをよりよく理解するには、この記事をご覧くださいvolatile

Lawrence Dolがあなたのことを明確に説明しましたread-write-update query

他のクエリについて

変数をシンクロナイズしてアクセスするよりも揮発性として宣言する方が適切なのはいつですか?

volatile私が日付変数について説明した例のように、すべてのスレッドがリアルタイムで変数の実際の値を取得する必要があると思われる場合は、使用する必要があります。

入力に依存する変数にvolatileを使用することは良い考えですか?

回答は最初のクエリと同じです。

理解を深めるには、この記事を参照してください。


したがって、読み取りは同時に発生する可能性があり、CPUはメインメモリをCPUスレッドキャッシュにキャッシュしないため、すべてのスレッドが最新の値を読み取りますが、書き込みについてはどうでしょうか。書き込みは同時に正しいものであってはなりませんか?2番目の質問:ブロックが同期されているが、変数が揮発性ではない場合、同期されたブロックの変数の値は、別のコードブロックの別のスレッドによって変更できますか?
the_prole

11

tl; dr

マルチスレッドには主に3つの問題があります。

1)レース条件

2)キャッシュ/古いメモリ

3)コンパイラとCPUの最適化

volatile2と3は解決できますが、1は解決できませんsynchronized。/explicitロックは1、2、3を解決できます。

詳細

1)このスレッドの安全でないコードを検討してください:

x++;

これは1つの操作のように見えますが、実際には3です。メモリからxの現在の値を読み取り、1を追加して、メモリに保存します。いくつかのスレッドが同時にそれを実行しようとすると、操作の結果は未定義になります。xもともと1だった場合、2つのスレッドがコードを操作した後、制御が他のスレッドに移される前に、どのスレッドが操作のどの部分を完了したかによって、2と3になることがあります。これは競合状態の形式です。

使用しsynchronizedたコードのブロックには、それが可能アトミック - 3つの操作が一度起こると、別のスレッドが途中で来て、干渉するための方法はありませんかのようにそれを作る意味します。その場合はx1で、2つのスレッドは、プリフォームしようとx++、我々は知っている、それは競合状態の問題を解決しますので、最終的に、それは3に等しくなります。

synchronized (this) {
   x++; // no problem now
}

マークxを付けvolatileてもx++;アトミックにならないため、この問題は解決されません。

2)さらに、スレッドには独自のコンテキストがあります。つまり、メインメモリから値をキャッシュできます。つまり、いくつかのスレッドは変数のコピーを持つことができますが、他のスレッド間で変数の新しい状態を共有せずに、作業コピーを操作します。

1つのスレッドで考えてみてくださいx = 10;。そして少し後で、別のスレッドでx = 20;。の値の変更xは最初のスレッドには表示されない可能性があります。これは、他のスレッドが新しい値を作業メモリに保存したが、メインメモリにコピーしていないためです。または、メインメモリにコピーしましたが、最初のスレッドは作業コピーを更新していません。したがって、最初のスレッドをチェックif (x == 20)すると、答えはになりますfalse

変数にマークを付けると、volatile基本的にすべてのスレッドがメインメモリでのみ読み取りおよび書き込み操作を実行するようになります。synchronizedすべてのスレッドに、ブロックに入るときにメインメモリから値を更新し、ブロックから出るときに結果をメインメモリにフラッシュするように指示します。

とにかくメインメモリへのフラッシュが発生するため、データの競合とは異なり、古いメモリは(再)生成が簡単ではないことに注意してください。

3)コンパイラーとCPUは(スレッド間のいかなる形式の同期もなしに)すべてのコードをシングルスレッドとして扱うことができます。つまり、マルチスレッドの側面で非常に意味のあるコードを調べて、それほど意味のないシングルスレッドのように扱うことができます。したがって、このコードが複数のスレッドで動作するように設計されていることがわからない場合は、最適化のためにコードを調べて、コードを並べ替えたり、一部を完全に削除したりすることもできます。

次のコードを検討してください。

boolean b = false;
int x = 10;

void threadA() {
    x = 20;
    b = true;
}

void threadB() {
    if (b) {
        System.out.println(x);
    }
}

が20に設定された後にのみtrueに設定されているため、threadBは20しか出力できない(または、threadB if-checkがbtrue に設定される前に実行される場合は何も出力されない)と考えるかもしれませんが、コンパイラ/ CPUが再注文する場合がありますthreadA、その場合、threadBも10を出力する可能性があります。再マーキング(または特定のケースでは破棄)されないことを保証するマークを付けます。つまり、threadBは20しか印刷できなかった(またはまったく印刷できなかった)ことになります。メソッドを同期済みとしてマークすると、同じ結果が得られます。また、変数を再順序付けしないことを保証するだけの変数をマークしますが、その前後のすべてを引き続き再順序付けできるため、いくつかのシナリオでは同期がより適しています。bxbvolatilevolatile

Java 5 New Memory Model以前は、volatileではこの問題は解決されていませんでした。


1
「1つの操作のように見えるかもしれませんが、実際には3です。xの現在の値をメモリから読み取り、1を加えて、メモリに保存します。」-そうです。メモリからの値を追加/変更するには、CPU回路を通過する必要があるためです。これは1つのアセンブリINC操作に変わるだけですが、基になるCPU操作は3倍であり、スレッドセーフのためにロックが必要です。いい視点ね。ただし、INC/DECコマンドはアセンブリでアトミックにフラグを立てることができ、それでも1つのアトミック操作になります。
ゾンビ

@Zombiesなので、x ++の同期ブロックを作成すると、フラグ付きのアトミックINC / DECに変換されますか、それとも通常のロックを使用しますか?
David Refaeli

知りません!私が知っているのは、INC / DECはアトミックではないということです。CPUの場合、他の算術演算と同じように、値をロードして読み取り、さらに(メモリに)書き込む必要があるためです。
ゾンビ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.