1. ホーム

[解決済み】atomic / volatile / synchronizedの違いは何ですか?

2022-03-26 23:15:59

質問

アトミック/ボラタイル/シンクロナイゼーションは内部でどのように動作するのですか?

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

コード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 ? また、スレッドセーフですか?

また、volatile変数/atomic変数に対する内部での読み書きの違いは何でしょうか?何かの記事で、スレッドが変数のローカルコピーを持っていると読みましたが、それは何でしょうか?

どのように解決するのですか?

具体的にどのようにするのかというご質問ですね。 内部動作 ということで、こちらです。

同期を取らない

private int counter;

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

基本的にはメモリから値を読み出し、インクリメントしてメモリに戻すというものです。これはシングルスレッドでは動作しますが、マルチコア、マルチ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 ( コンペア・アンド・スワップ これらは、現在の値が他の何かと等しい(そして正常に返される)場合にのみ、特定の変数を変更することを可能にします。ですから 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;
}

上記と同じ問題ですが、さらに悪いことに iprivate . レースコンディションはまだ存在しています。なぜ問題なのでしょうか?例えば、2つのスレッドが同時にこのコードを実行した場合、出力は次のようになります。 + 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 は事実上何の効果もありません。

たとえ、最終変数(または this ) を使って同期をとっていますが、このコードはまだ正しくありません。2つのスレッドが最初に i から temp を同期的に使用します(ローカルに同じ値を持つ temp に新しい値を代入する。 i (例えば、1から6まで)、もう一方は同じことを(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;
  }
}