Javaの原子変数の紹介
1. 前書き
簡単に言えば、並行状態が関係している場合、共有状態は非常に簡単に問題につながります。 共有された可変オブジェクトへのアクセスが適切に管理されていない場合、アプリケーションは検出が困難な同時実行エラーをすぐに起こしやすくなります。
この記事では、同時アクセスを処理するためのロックの使用について再検討し、ロックに関連するいくつかの欠点を探り、最後に、代替手段としてアトミック変数を紹介します。
2. ロック
クラスを見てみましょう。
public class Counter {
int counter;
public void increment() {
counter++;
}
}
シングルスレッド環境の場合、これは完全に機能します。ただし、複数のスレッドに書き込みを許可するとすぐに、一貫性のない結果が得られます。
これは、単純なインクリメント操作(counter++)が原因で、アトミック操作のように見えますが、実際には、値の取得、インクリメント、更新された値の書き戻しの3つの操作の組み合わせです。
2つのスレッドが同時に値を取得して更新しようとすると、更新が失われる可能性があります。
オブジェクトへのアクセスを管理する方法の1つは、ロックを使用することです。 これは、incrementメソッドシグネチャでsynchronizedキーワードを使用することで実現できます。 synchronizedキーワードは、一度に1つのスレッドのみがメソッドに入ることができるようにします(ロックと同期の詳細については、–Guide to Synchronized Keyword in Javaを参照してください)。
public class SafeCounterWithLock {
private volatile int counter;
public synchronized void increment() {
counter++;
}
}
さらに、スレッド間で適切な参照の可視性を確保するために、volatileキーワードを追加する必要があります。
ロックを使用すると問題が解決します。 ただし、パフォーマンスは打撃を受けます。
複数のスレッドがロックを取得しようとすると、そのうちの1つが勝ち、残りのスレッドはブロックまたは中断されます。
The process of suspending and then resuming a thread is very expensiveであり、システムの全体的な効率に影響します。
counterなどの小さなプログラムでは、コンテキストの切り替えに費やされる時間が実際のコード実行よりもはるかに長くなる可能性があるため、全体的な効率が大幅に低下します。
3. アトミックオペレーション
並行環境向けのノンブロッキングアルゴリズムの作成に焦点を合わせた研究分野があります。 これらのアルゴリズムは、比較とスワップ(CAS)などの低レベルのアトミックマシン命令を活用して、データの整合性を確保します。
一般的なCAS操作は、次の3つのオペランドで機能します。
-
操作するメモリ位置(M)
-
変数の既存の期待値(A)
-
設定する必要がある新しい値(B)
CAS操作は、Mの値をBにアトミックに更新しますが、Mの既存の値がAと一致する場合にのみ、それ以外の場合はアクションは実行されません。
どちらの場合も、Mの既存の値が返されます。 これは、3つのステップ(値の取得、値の比較、値の更新)を1つのマシンレベルの操作に結合します。
複数のスレッドがCASを介して同じ値を更新しようとすると、そのうちの1つが勝ち、値を更新します。 However, unlike in the case of locks, no other thread gets suspended;代わりに、値を更新できなかったことが通知されます。 スレッドはさらに作業を進めることができ、コンテキストの切り替えは完全に回避されます。
もう1つの結果は、コアプログラムロジックがより複雑になることです。 これは、CAS操作が成功しなかった場合のシナリオを処理する必要があるためです。 成功するまで何度も再試行するか、ユースケースに応じて何もせずに先に進むことができます。
4. Javaのアトミック変数
Javaで最も一般的に使用されるアトミック変数クラスは、AtomicInteger、AtomicLong、AtomicBoolean、およびAtomicReferenceです。 これらのクラスは、それぞれint、long、boolean、およびアトミックに更新可能なオブジェクト参照を表します。 これらのクラスによって公開される主なメソッドは次のとおりです。
-
get() –メモリから値を取得するため、他のスレッドによって行われた変更が表示されます。 volatile変数の読み取りと同等
-
set() –値をメモリに書き込み、変更が他のスレッドに表示されるようにします。 volatile変数を書き込むのと同じです
-
lazySet() –最終的に値をメモリに書き込み、後続の関連するメモリ操作で並べ替えることができます。 使用例の1つは、ガベージコレクションのために参照を無効化することです。これは、再びアクセスされることはありません。 この場合、nullvolatileの書き込みを遅らせることで、パフォーマンスが向上します。
-
compareAndSet() –セクション3で説明したのと同じように、成功するとtrueを返し、それ以外の場合はfalseを返します。
-
weakCompareAndSet() –セクション3で説明したものと同じですが、順序付けの前に発生しないという意味で弱いです。 これは、他の変数に加えられた更新を必ずしも見るとは限らないことを意味します
AtomicIntegerで実装されたスレッドセーフカウンターを以下の例に示します。
public class SafeCounterWithoutLock {
private final AtomicInteger counter = new AtomicInteger(0);
public int getValue() {
return counter.get();
}
public void increment() {
while(true) {
int existingValue = getValue();
int newValue = existingValue + 1;
if(counter.compareAndSet(existingValue, newValue)) {
return;
}
}
}
}
ご覧のとおり、incrementメソッドの呼び出しで値が常に1増加することを保証したいので、失敗時にcompareAndSet操作を再試行します。
5. 結論
このクイックチュートリアルでは、ロックに関連する欠点を回避できる並行処理の代替方法について説明しました。 また、Javaのアトミック変数クラスによって公開される主なメソッドも調べました。
いつものように、例はすべて利用可能なover on GitHubです。
内部で非ブロッキングアルゴリズムを使用するクラスをさらに調べるには、a guide to ConcurrentMapを参照してください。