アトミック操作の出現の前提条件

アトミック操作がどのように機能するかを理解するために、この例を見てみましょう。

public class Counter {
    int count;

    public void increment() {
        count++;
    }
}

スレッドが 1 つあるときは、すべてがうまく機能しますが、マルチスレッドを追加すると、間違った結果が得られます。これは、インクリメント操作が 1 つの操作ではなく、現在の値を取得するリクエストという 3 つの操作であるためです。カウント、次に 1 ずつインクリメントし、再度書き込みます。カウント

また、2 つのスレッドが変数をインクリメントしようとすると、データが失われる可能性が高くなります。つまり、両方のスレッドが 100 を受け取り、その結果、両方とも期待値 102 の代わりに 101 を書き込みます。

そしてそれをどうやって解決するのでしょうか?ロックを使用する必要があります。synchronizedキーワードはこの問題の解決に役立ちます。これを使用すると、一度に 1 つのスレッドがメソッドにアクセスすることが保証されます。

public class SynchronizedCounterWithLock {
    private volatile int count;

    public synchronized void increment() {
        count++;
    }
}

さらに、スレッド間で参照が正しく表示されるようにするために、volatileキーワードを追加する必要があります。私たちは上で彼の作品をレビューしました。

しかし、まだ欠点もあります。最大の要因はパフォーマンスです。多くのスレッドがロックを取得しようとしていて、そのうちの 1 つが書き込みの機会を得た時点で、スレッドが解放されるまで残りのスレッドはブロックされるか一時停止されます。

これらすべてのプロセス、ブロック、別のステータスへの切り替えは、システムのパフォーマンスにとって非常に高価です。

アトミック操作

このアルゴリズムでは、コンペア アンド スワップ (CAS、データの整合性を保証するコンペア アンド スワップ。これについてはすでに大量の研究が行われています) などの低レベルの機械命令が使用されます。

一般的な CAS 操作は、次の 3 つのオペランドで動作します。

  • 作業用メモリ空間(M)
  • 変数の既存の期待値 (A)
  • 新たに設定する値(B)

CAS は M を B にアトミックに更新しますが、M の値が A と同じである場合に限り、それ以外の場合はアクションは実行されません。

1 番目と 2 番目のケースでは M の値が返されるため、値の取得、値の比較、更新の 3 つの手順を組み合わせることができます。そして、それはすべてマシンレベルでの 1 つの操作になります。

マルチスレッド アプリケーションが変数にアクセスして更新しようとし、CAS が適用されると、スレッドの 1 つが変数を取得して更新できるようになります。ただし、ロックとは異なり、他のスレッドは値を更新できないというエラーを受け取るだけです。その後、次の作業に進むことになりますが、この種の作業では切り替えは完全に排除されます。

この場合、CAS 操作が正常に機能しなかった場合の状況に対処する必要があるため、ロジックはさらに難しくなります。操作が成功するまでコードが先に進まないようにコードをモデル化します。

アトミックタイプの概要

int型の最も単純な変数の同期を設定する必要がある状況に遭遇したことがありますか?

すでに説明した最初の方法は、volatile + synchronizedを使用することです。ただし、特別な Atomic* クラスもあります。

CAS を使用すると、最初の方法と比べて操作が速くなります。さらに、値を追加したり、インクリメントおよびデクリメント操作を行うための特別で非常に便利なメソッドもあります。

AtomicBoolean AtomicInteger AtomicLong AtomicIntegerArray AtomicLongArrayは、操作がアトミックであるクラスです。以下では、彼らとの仕事を分析します。

アトミック整数

AtomicIntegerクラスは、拡張されたアトミック操作に加えて、アトミックに読み書きできるint値に対する操作を提供します。

変数の読み取りと書き込みと同様に機能するgetメソッドとsetメソッドがあります。

つまり、前に説明した同じ変数のその後の受信が「前に発生」します。アトミックのCompareAndSetメソッドには、次のようなメモリ整合性機能もあります。

新しい値を返すすべての操作はアトミックに実行されます。

int addAndGet (int デルタ) 現在の値に特定の値を加算します。
boolean CompareAndSet(期待される int、更新される int) 現在の値が期待値と一致する場合、値を指定された更新された値に設定します。
int decrementAndGet() 現在の値を 1 つ減らします。
int getAndAdd(int デルタ) 指定された値を現在の値に加算します。
int getAndDecrement() 現在の値を 1 つ減らします。
int getAndIncrement() 現在の値を 1 つ増やします。
int getAndSet(int newValue) 指定された値を設定し、古い値を返します。
int incrementAndGet() 現在の値を 1 つ増やします。
LazySet(int newValue) 最後に指定された値に設定します。
booleanweakCompareAndSet(期待値、更新整数) 現在の値が期待値と一致する場合、値を指定された更新された値に設定します。

例:

ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);

System.out.println(atomicInteger.get()); // prints 50