AtomicIntegerおよび他のAtomic変数が同時アクセスを許可することを理解しています。このクラスは通常どのような場合に使用されますか?
AtomicInteger
には主に2つの用途があります。
多くのスレッドが同時に使用できるアトミックカウンター(incrementAndGet()
など)として
compare-and-swap 命令(compareAndSet()
)をサポートし、ノンブロッキングアルゴリズムを実装するプリミティブとして。
BrianGöetzのJava Concurrency In Practice の非ブロッキング乱数ジェネレーターの例を次に示します。
public class AtomicPseudoRandom extends PseudoRandom {
private AtomicInteger seed;
AtomicPseudoRandom(int seed) {
this.seed = new AtomicInteger(seed);
}
public int nextInt(int n) {
while (true) {
int s = seed.get();
int nextSeed = calculateNext(s);
if (seed.compareAndSet(s, nextSeed)) {
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
...
}
ご覧のとおり、基本的にincrementAndGet()
とほぼ同じように機能しますが、インクリメントの代わりに任意の計算(calculateNext()
)を実行します(そして、結果を処理してから戻ります)。
私が考えることができる絶対的な最も簡単な例は、アトミック操作をインクリメントすることです。
標準intの場合:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
AtomicIntegerの場合:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
後者は、すべてのアクセスの同期に頼ることなく、単純な突然変異効果(特にカウント、または一意のインデックス付け)を実行する非常に簡単な方法です。
オプティミスティックロックのタイプとしてcompareAndSet()
を使用することにより、より複雑な同期のないロジックを使用できます-現在の値を取得し、これに基づいて結果を計算し、この結果を設定しますiff value計算、それ以外の場合は再度開始-しかし、カウントの例は非常に便利で、複数のスレッドが関与しているというヒントがある場合、カウントにAtomicIntegers
を使用し、VM全体で一意のジェネレーターを使用することがよくあります。 dは、プレーンなints
を使用するのが時期尚早な最適化であると考えています。
ints
および適切なsynchronized
宣言を使用すると、ほぼ常に同じ同期の保証を達成できますが、AtomicInteger
の利点は、スレッドセーフが実際のオブジェクト自体に組み込まれていることです。 int
値にアクセスするすべてのメソッドの。 i++
を返し、事前に正しいモニターセットを取得することを覚えている(または覚えていない)ときよりも、getAndIncrement()
を呼び出すときに誤ってスレッドセーフに違反することははるかに困難です。
AtomicIntegerのメソッドを見ると、intの一般的な操作に対応する傾向があることがわかります。例えば:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
これはスレッドセーフバージョンです。
static int i;
// Later, in a thread
int current = ++i;
メソッドは次のようにマッピングされます。++i
はi.incrementAndGet()
ですi++
はi.getAndIncrement()
です--i
はi.decrementAndGet()
ですi--
はi.getAndDecrement()
ですi = x
はi.set(x)
ですx = i
はx = i.get()
です
compareAndSet
やaddAndGet
のような他の便利なメソッドもあります
AtomicInteger
の主な用途は、マルチスレッドコンテキストで、synchronized
を使用せずに整数に対してスレッドセーフな操作を実行する必要がある場合です。プリミティブ型int
の割り当てと取得はすでにアトミックですが、AtomicInteger
には、int
のアトミックではない多くの操作が付属しています。
最も単純なものはgetAndXXX
またはxXXAndGet
です。たとえば、getAndIncrement()
はi++
とアトミックに相当しますが、実際には3つの操作(検索、追加、割り当て)のショートカットであるためアトミックではありません。 compareAndSet
は、セマフォ、ロック、ラッチなどの実装に非常に役立ちます。
AtomicInteger
を使用すると、同期を使用して同じことを実行するよりも高速で読みやすくなります。
簡単なテスト:
public synchronized int incrementNotAtomic() {
return notAtomic++;
}
public void performTestNotAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
incrementNotAtomic();
}
System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}
public void performTestAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
atomic.getAndIncrement();
}
System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}
Java 1.6を搭載したPCでは、アトミックテストは3秒で実行されますが、同期テストは約5.5秒で実行されます。ここでの問題は、同期する操作(notAtomic++
)が本当に短いことです。そのため、同期のコストは操作と比較して非常に重要です。
AtomicIntegerのほかに、たとえばInteger
sの値として、Map
の可変バージョンとしてAtomicIntegerを使用できます。
たとえば、あるクラスのインスタンスを生成するライブラリがあります。これらのインスタンスはサーバーに送信されるコマンドを表すため、これらの各インスタンスには一意の整数IDが必要であり、各コマンドには一意のIDが必要です。複数のスレッドが同時にコマンドを送信できるため、AtomicIntegerを使用してこれらのIDを生成します。別のアプローチとしては、ある種のロックと通常の整数を使用する方法がありますが、それは低速でエレガントではありません。
Gabuzoが言ったように、参照によってintを渡したいとき、時々AtomicIntegersを使用します。これは、アーキテクチャ固有のコードを持つ組み込みクラスであるため、簡単にコード化できるMutableIntegerよりも簡単で、おそらく最適化されています。とはいえ、それはクラスの虐待のように感じます。
Java 8では、2つの興味深い関数でアトミッククラスが拡張されています。
どちらもupdateFunctionを使用してアトミック値の更新を実行しています。違いは、最初の値が古い値を返し、2番目の値が新しい値を返すことです。 updateFunctionは、標準の操作よりも複雑な「比較と設定」操作を実行するために実装できます。たとえば、アトミックカウンターが0未満にならないことを確認できます。通常は同期が必要であり、ここではコードはロックされていません。
public class Counter {
private final AtomicInteger number;
public Counter(int number) {
this.number = new AtomicInteger(number);
}
/** @return true if still can decrease */
public boolean dec() {
// updateAndGet(fn) executed atomically:
return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
}
}
コードは Java Atomic Example から取得されます。
複数のスレッドからアクセスまたは作成できるオブジェクトにIDを付与する必要がある場合、通常AtomicIntegerを使用します。通常、オブジェクトのコンストラクターでアクセスするクラスの静的属性として使用します。
アトミック整数またはロングに対してcompareAndSwap(CAS)を使用して、非ブロッキングロックを実装できます。 "Tl2" Software Transactional Memory 紙はこれを説明しています:
特別なバージョンの書き込みロックをすべてのトランザクションメモリの場所に関連付けます。最も単純な形式では、バージョン管理された書き込みロックは、CAS操作を使用してロックを取得し、ストアをロックを解除する単一のWordスピンロックです。ロックが取得されたことを示すために必要なのは1ビットだけなので、残りのロックWordを使用してバージョン番号を保持します。
説明しているのは、最初にアトミック整数を読み取ることです。これを無視されたロックビットとバージョン番号に分割します。 CASは、現在のバージョン番号とロックビットセット、および次のバージョン番号でロックビットがクリアされているため、CASを書き込みます。あなたが成功し、あなたがロックを所有するスレッドになるまでループします。ロックビットをクリアした状態で現在のバージョン番号を設定してロックを解除します。このペーパーでは、ロックのバージョン番号を使用して、スレッドが書き込み時に一貫した読み取りセットを持つように調整することについて説明しています。
この記事 は、プロセッサが比較およびスワップ操作をハードウェアでサポートしており、非常に効率的であることを説明しています。また、次のことも主張しています。
アトミック変数を使用するノンブロッキングCASベースのカウンターは、低から中程度の競合でロックベースのカウンターよりもパフォーマンスが優れています
重要なのは、安全に同時アクセスと変更を許可することです。それらは一般にマルチスレッド環境でカウンターとして使用されます-導入前は、同期ブロック内のさまざまなメソッドをラップするユーザー作成クラスでなければなりませんでした。
私はAtomicIntegerを使用して、食事哲学者の問題を解決しました。
私のソリューションでは、フォークを表すためにAtomicIntegerインスタンスが使用されました。哲学者ごとに2つ必要です。各哲学者は、1〜5の整数として識別されます。フォークが哲学者によって使用されると、AtomicIntegerは1〜5の哲学者の値を保持します。 。
AtomicIntegerを使用すると、1つのアトミック操作で、フォークが解放されているかどうか(value ==-1)を確認し、解放されている場合はフォークの所有者に設定できます。以下のコードを参照してください。
AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){
if (Hungry) {
//if fork is free (==-1) then grab it by denoting who took it
if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
//at least one fork was not succesfully grabbed, release both and try again later
fork0.compareAndSet(p, -1);
fork1.compareAndSet(p, -1);
try {
synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork
lock.wait();//try again later, goes back up the loop
}
} catch (InterruptedException e) {}
} else {
//sucessfully grabbed both forks
transition(fork_l_free_and_fork_r_free);
}
}
}
CompareAndSetメソッドはブロックしないため、スループットが向上し、より多くの作業が行われます。ご存知かもしれませんが、食事をする哲学者の問題は、プロセスを続けるためにリソースが必要なように、リソースへのアクセスを制御する必要がある場合、つまりフォークが必要な場合に使用されます。