Java Language
原子タイプ
サーチ…
前書き
Java Atomic Typesは、ロックに頼ることなくスレッドセーフでアトミックな基本操作を提供するシンプルな可変型です。ロックが同時性のボトルネックとなる場合や、デッドロックやライブロックの危険がある場合に使用することを意図しています。
パラメーター
パラメータ | 説明 |
---|---|
セット | フィールドの揮発性のセット |
取得する | フィールドの揮発性読み取り |
レイジーセット | これは、フィールドの店舗注文オペレーションです |
compareAndSet | 値がexpeed値ならば、それを新しい値に送る |
getAndSet | 現在の値を取得して更新する |
備考
本質的には、揮発性の読み取りまたは書き込みとCAS操作の組み合わせが多数あります。これを理解する最も良い方法は、ソースコードを直接見ることです。例: AtomicInteger 、 Unsafe.getAndSet
原子タイプの作成
単純なマルチスレッドコードでは、 同期を使用することは許容されます。しかし、同期を使用することは活力に影響し、コードベースが複雑になるにつれ、 デッドロック 、 飢餓、またはLivelockで終わる可能性が高くなります。
より複雑な並行処理の場合は、同期メソッドまたはコードブロックを使用するオーバーヘッドなしに個々の変数にスレッドセーフな方法でアクセスできるため、Atomic Variablesを使用する方が優れています。
AtomicInteger
型の作成:
AtomicInteger aInt = new AtomicInteger() // Create with default value 0
AtomicInteger aInt = new AtomicInteger(1) // Create with initial value 1
他のインスタンスの場合も同様です。
AtomicIntegerArray aIntArray = new AtomicIntegerArray(10) // Create array of specific length
AtomicIntegerArray aIntArray = new AtomicIntegerArray(new int[] {1, 2, 3}) // Initialize array with another array
他の原子タイプについても同様です。
float
型とdouble
型がないという顕著な例外があります。これらは、使用してシミュレートすることができFloat.floatToIntBits(float)
とFloat.intBitsToFloat(int)
用float
などDouble.doubleToLongBits(double)
とDouble.longBitsToDouble(long)
ダブルスのために。
sun.misc.Unsafe
を使用する場合は、 sun.misc.Unsafe
アトミック操作を使用して、原始変数をアトミックとして使用できます。すべてのプリミティブ型は、このように使用するために、int型またはlong型で変換またはエンコードする必要があります。詳細については、 sun.misc.Unsafeを参照してください。
原子タイプの動機づけ
マルチスレッドアプリケーションを実装する簡単な方法は、Javaの組み込みの同期およびロックプリミティブを使用することです。たとえば、 synchronized
キーワードです。次の例は、 synchronized
を使用してカウントを累積する方法を示しています。
public class Counters {
private final int[] counters;
public Counters(int nosCounters) {
counters = new int[nosCounters];
}
/**
* Increments the integer at the given index
*/
public synchronized void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number]++;
}
}
/**
* Obtains the current count of the number at the given index,
* or if there is no number at that index, returns 0.
*/
public synchronized int getCount(int number) {
return (number >= 0 && number < counters.length) ? counters[number] : 0;
}
}
この実装は正しく動作します。ただし、多数のスレッドが同じCounters
オブジェクトで同時に多数の呼び出しを行っている場合は、同期がボトルネックになりやすくなります。具体的には:
- 各
synchronized
メソッド呼び出しは、現在のスレッドがCounters
インスタンスのロックを取得して開始します。 - スレッドは、
number
値を確認している間にロックを保持し、カウンタを更新します。 - 最後に、ロックを解除し、他のスレッドにアクセスを許可します。
1つのスレッドが別のスレッドを保持している間にロックを取得しようとすると、ロックが解除されるまで、試行スレッドはステップ1でブロック(停止)されます。複数のスレッドが待機している場合、それらのスレッドの1つが取得し、他のスレッドは引き続きブロックされます。
これはいくつかの問題につながります:
ロックに多量の競合がある場合(つまり、多くのスレッドが獲得しようとすると)、一部のスレッドは長時間ブロックされる可能性があります。
スレッドがロックを待ってブロックされると、オペレーティングシステムは通常、別のスレッドにスイッチの実行を試みます。このコンテキスト切り替えは、プロセッサに比較的大きなパフォーマンスの影響を与えます。
同じロック上に複数のスレッドがブロックされている場合、それらのスレッドのいずれかが「公平」に扱われる(つまり、各スレッドの実行が保証される)ことは保証されません。これはスレッドの飢餓につながります 。
どのように原子型を実装するのですか?
AtomicInteger
カウンタを使用して上の例を書き直してみましょう。
public class Counters {
private final AtomicInteger[] counters;
public Counters(int nosCounters) {
counters = new AtomicInteger[nosCounters];
for (int i = 0; i < nosCounters; i++) {
counters[i] = new AtomicInteger();
}
}
/**
* Increments the integer at the given index
*/
public void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number].incrementAndGet();
}
}
/**
* Obtains the current count of the object at the given index,
* or if there is no number at that index, returns 0.
*/
public int getCount(int number) {
return (number >= 0 && number < counters.length) ?
counters[number].get() : 0;
}
}
int[]
をAtomicInteger[]
に置き換え、それを各要素のインスタンスで初期化しました。また、 int
値の操作の代わりにincrementAndGet()
とget()
呼び出しを追加しました。
しかし、最も重要なことは、ロックがもはや必要でないため、 synchronized
キーワードを削除できることです。これは、 incrementAndGet()
およびget()
操作がアトミックでスレッドセーフであるために機能します。この文脈では、それは:
配列内の各カウンタは、操作の前の状態(「インクリメント」など)または「後」状態のいずれかでのみ観測可能です。
オペレーションが時刻
T
に発生すると仮定すると、スレッドは時刻T
後の「前」状態を見ることができない。
さらに、2つのスレッドが実際に同じAtomicInteger
インスタンスを同時に更新しようとしている間も、オペレーションの実装は、指定されたインスタンスに対して一度に1つの増分だけが確実に行われるようにします。これはロックなしで行われるため、パフォーマンスが向上することがあります。
原子タイプはどのように機能しますか?
アトミックタイプは、通常、ターゲットマシンの命令セットの特殊なハードウェア命令に依存します。例えば、Intelベースの命令セットは、特定の一連のメモリ操作を原子的に実行するCAS
( 比較およびスワップ )命令を提供する。
これらの低レベルの命令は、それぞれのAtomicXxx
クラスのAPIで高レベルの演算を実装するために使用されます。たとえば、(やはりCのような擬似コードで):
private volatile num;
int increment() {
while (TRUE) {
int old = num;
int new = old + 1;
if (old == compare_and_swap(&num, old, new)) {
return new;
}
}
}
AtomicXxxx
に競合がない場合、 if
テストは成功し、ループはただちに終了します。競合がある場合、 if
はスレッドの1つを除くすべてのスレッドで失敗し、ループのサイクル数が少ない場合はループ内で「回転」します。実際には、スピンが桁違いに速くなります(非同期的に高レベルの競合を除いて、同期は原子クラスよりも優れていますが、CAS操作が失敗した場合、再試行では競合が増えるだけです) 1。
ちなみに、CAS命令は、通常、JVMが非競合のロックを実装するために使用します。ロックが現在ロックされていないことがJVMによって分かると、CASを使用してロックを取得しようとします。 CASが成功すれば、高価なスレッドのスケジューリング、コンテキストの切り替えなどを行う必要はありません。使用されるテクニックの詳細については、「 HotSpotのバイアスされたロック」を参照してください。