原子 / 揮発性 / 同期の違いは何ですか? 質問する

原子 / 揮発性 / 同期の違いは何ですか? 質問する

内部的にatomic / volatile / synchronizedはどのように機能しますか?

次のコードブロックの違いは何ですか?

コード 1

private int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

コード2

private AtomicInteger counter;

public int getNextUniqueIndex() {
    return counter.getAndIncrement();
}

コード3

private volatile int counter;

public int getNextUniqueIndex() {
    return counter++; 
}

volatile次のように動作しますか?

volatile int i = 0;
void incIBy5() {
    i += 5;
}

に相当

Integer i = 5;
void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

2 つのスレッドが同時に同期ブロックに入ることはできないと思いますが、正しいでしょうか? これが本当なら、atomic.incrementAndGet()なしではどのように動作しますかsynchronized? また、スレッドセーフですか?

また、揮発性変数/アトミック変数の内部読み取りと書き込みの違いは何ですか? スレッドには変数のローカル コピーがあるとある記事で読みましたが、それは何ですか?

ベストアンサー1

具体的には、内部的にどのように機能するかについて質問されているので、次のようになります。

同期なし

private int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

基本的に、メモリから値を読み取り、増分してメモリに戻します。これはシングル スレッドでは機能しますが、マルチコア、マルチ CPU、マルチレベル キャッシュの時代では正しく機能しません。まず、競合状態 (複数のスレッドが同時に値を読み取ることができる) が発生しますが、可視性の問題も発生します。値は「ローカル」CPU メモリ (一部のキャッシュ) にのみ保存され、他の CPU/コア (つまりスレッド) には表示されない可能性があります。これが、多くの人がスレッド内の変数のローカル コピーを参照する理由です。これは非常に危険です。次のよく使われるが壊れたスレッド停止コードを検討してください。

private boolean stopped;

public void run() {
    while(!stopped) {
        //do some work
    }
}

public void pleaseStop() {
    stopped = true;
}

volatile変数に追加するstoppedと正常に動作します。他のスレッドがメソッドstoppedを介して変数を変更するとpleaseStop()、その変更が作業スレッドのループですぐに確認できるようになりますwhile(!stopped)。ちなみに、これはスレッドを中断する良い方法ではありません。以下を参照してください。役に立たないまま永遠に実行されているスレッドを停止する方法そして特定のJavaスレッドを停止する

AtomicInteger

private AtomicInteger counter = new AtomicInteger();

public int getNextUniqueIndex() {
  return counter.getAndIncrement();
}

このAtomicIntegerクラスではCAS(比較と交換) 低レベルの CPU 操作 (同期は必要ありません!) を使用すると、現在の値が他の値と等しい (かつ正常に返される) 場合にのみ、特定の変数を変更できます。したがって、実行すると、getAndIncrement()実際にはループ内で実行されます (実際の実装は簡略化されています)。

int current;
do {
  current = get();
} while(!compareAndSet(current, current + 1));

つまり、基本的には、読み取り、増分された値の保存を試行し、成功しない場合 (値が と等しくなくなった場合current)、読み取り、再試行します。 はcompareAndSet()ネイティブ コード (アセンブリ) で実装されます。

volatile同期なし

private volatile int counter;

public int getNextUniqueIndex() {
  return counter++; 
}

このコードは正しくありません。可視性の問題は修正されます(volatile他のスレッドが変更を確認できるようになりますcounter)が、競合状態が残っています。これは説明複数回: 事前/事後増分はアトミックではありません。

の唯一の副作用は、他のすべてのパーティがデータの最新バージョンを見ることができるように、キャッシュをvolatileフラッシュvolatile」することです。これは、ほとんどの状況では厳しすぎるため、 はデフォルトでありません。

volatile同期なし(2)

volatile int i = 0;
void incIBy5() {
  i += 5;
}

上記と同じ問題ですが、iではないためさらに悪いです。競合状態は依然として存在します。なぜ問題になるのでしょうか? たとえば、2 つのスレッドがこのコードを同時に実行した場合、出力はまたは にprivateなる可能性があります。ただし、変更が確実に表示されます。+ 5+ 10

複数の独立したsynchronized

void incIBy5() {
  int temp;
  synchronized(i) { temp = i }
  synchronized(i) { i = temp + 5 }
}

驚いたことに、このコードも間違っています。実際、完全に間違っています。まず、 を同期していますがi、これは変更されようとしています (さらに、iはプリミティブなので、オートボクシングによって作成された一時的な を同期していると思いますInteger...)。完全に間違っています。次のように書くこともできます。

synchronized(new Object()) {
  //thread-safe, SRSLy?
}

2 つのスレッドが同じロックでsynchronized同じブロックに入ることはできません。この場合 (およびコード内でも同様)、ロック オブジェクトは実行ごとに変更されるため、実質的に効果はありません。synchronized

同期にfinal 変数 (または ) を使用した場合でもthis、コードは依然として正しくありません。2 つのスレッドは、最初にを同期的iに読み取りtemp( にローカルに同じ値を持つtemp)、最初のスレッドが に新しい値i(たとえば、1 から 6) を割り当て、もう 1 つのスレッドが同じこと (1 から 6) を実行します。

同期は、読み取りから値の割り当てまで行う必要があります。最初の同期は効果がなく (読み取りはintアトミック)、2 番目も同様です。私の意見では、これらは正しい形式です。

void synchronized incIBy5() {
  i += 5 
}

void incIBy5() {
  synchronized(this) {
    i += 5 
  }
}

void incIBy5() {
  synchronized(this) {
    int temp = i;
    i = temp + 5;
  }
}

おすすめ記事