1つのアトミック操作クラス
Javaは、JDK 1.5以降にjava.util.concurrent.atomicパッケージ(以下、Atomicパッケージと呼びます)を提供しています。このパッケージのアトミック操作クラスは、変数を更新するためのシンプルで効率的でスレッドセーフな方法を提供します。変数には多くの種類があるため、アトミック更新メソッドには、アトミック更新基本型、アトミック更新配列、アトミック更新参照、およびアトミック更新属性(フィールド)の4種類があります。
1.1アトミックアップデートの基本的なタイプ
- AtomicBoolean:アトミック更新ブール型。
- AtomicInteger:アトミック更新整数。
- AtomicLong:アトミック更新長整数。
方法 | 効果 |
---|---|
int addAndGet(int delta) | 入力した値をインスタンスの値(AtomicIntegerの値)にアトミックに追加し、結果を返します。 |
boolean compareAndSet(int expect、int update) | 入力された値が期待値と等しい場合、値は入力された値にアトミックに設定されます。 |
int getAndIncrement() | 現在の値にアトミックに1を加算します。ここで返される値は増分前の値であることに注意してください。 |
int getAndSet(int newValue) | 原子的にnewValueの値に設定し、古い値を返します。 |
1.2アレイを原子的に更新する
配列内の要素をアトミックに更新するために、Atomicパッケージは次の4つのクラスを提供します。
- AtomicIntegerArray:整数配列の要素のアトミック更新。
- AtomicLongArray:Atomicは長整数配列の要素を更新します。
- AtomicReferenceArray:参照型配列の要素のアトミック更新
方法 | 効果 |
---|---|
int addAndGet(int i、int delta) | 配列のインデックスiの要素に入力値を原子的に追加します |
boolean compareAndSet(int i、int expect、int update) | 現在の値が期待値と等しい場合、 配列の位置iの要素はアトミックに更新値に設定されます |
1.3 原子更新引用
基本型のAtomicIntegerは、1つの変数しか更新できません。複数の変数をアトミックに更新する場合は、このアトムを使用して、参照型によって提供されるクラスを更新する必要があります。Atomicパッケージには、次の3つのカテゴリがあります。
- AtomicReference:アトミック更新参照型。
- AtomicReferenceFieldUpdater:参照型のフィールドをアトミック更新します。
- AtomicMarkableReference:マークビットを含むアトミック更新参照型。ブールフラグと参照型はアトミックに更新できます。構築方法はAtomicMarkableReference(V initialRef、boolean initialMark)です。
1.4アトミックアップデートフィールド
クラス内のフィールドをアトミックに更新する必要がある場合は、アトミック更新フィールドクラスを使用する必要があります。Atomicパッケージは、アトミックフィールド更新用に次の3つのクラスを提供します。
- AtomicIntegerFieldUpdater:整数フィールドをアトミックに更新するアップデーター。
- AtomicLongFieldUpdater:長整数フィールドをアトミックに更新するアップデーター。
- AtomicStampedReference:バージョン番号付きのアトミック更新参照タイプ。このクラスは、整数値を参照に関連付けます。これは、アトミック更新データとデータバージョン番号に使用でき、アトミック更新にCASを使用するときに発生する可能性のあるABA問題を解決できます。
2. CAS
2.1概要
CAS(Compare-And-Swap)は、単語の意味を調べ、比較して置き換えます。比較と置換は、期待値を変数の現在の値と比較することです。現在の変数の値が期待値と等しい場合は、現在の変数の値を新しい値に置き換えます。java.util.concurrent.atomicパッケージはすべてCASの概念を使用しています。
これは、CPUの並行性プリミティブです。プリミティブはオペレーティングシステムのカテゴリに属し、特定の機能のプロセスを完了するためのいくつかの命令で構成されています。プリミティブの実行は継続的である必要があり、実行中に中断されてはなりません。言い換えれば、CASはデータの不整合を引き起こさず、CASはスレッドセーフです
CASには、メモリ値V、古い期待値A、および変更する更新値Bの3つのオペランドがあります。期待値がメモリ値Vと同じである場合に限り、メモリ値をBに変更します。それ以外の場合は変更します。何もありません。。
2.2CASの使用
AtomicInteger atomicInteger = new AtomicInteger(10);
System.out.println(atomicInteger.compareAndSet(10, 2020) + "\t当前的值" + atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(10, 2019) + "\t当前的值" + atomicInteger.get());
CASの役割は、現在の作業メモリーの値をメインの物理メモリーの値と比較することです。同じ場合は、指定した値に更新します。それ以外の場合は、作業メモリーとメインの値まで比較を続けます。物理メモリは同じです。私たちは通常それを呼びます:比較して交換します
CASには2つのパラメーターがあり、1つ目は期待値で、2つ目は変更された値です。
2.3CASの基本原則
AtomicIntegerにはgetAndIncrement()というメソッドがあります。このメソッドは同期せずに安全性を保証できます。volatileが原子性を保証しないという解決策についてはすでに説明しました。次に、この方法を使用して、CASの基本原則を確認します。
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS的底层使用了unsafe类,由于java需要通过本地(Native)方法才能访问底层系统,unsafe就是这个后门,通过unsafe可以直接操作特定的内存数据。看源码也可以知道unsafe有一堆的本地方法。
安全でないgetAndAddIntメソッドは、次の3つのパラメーターを渡す必要があります。
- var1:現在のオブジェクト
- var2(valueOffset):メモリ内の変数のオフセットアドレスを表します
- var4:変更する値
ローカルメソッドであるgetAndAddIntメソッドには、ループ判定メソッドcompareAndSwapIntがあります。4つのパラメータを渡します
- var1:AtomicIntegerオブジェクト自体
- var2:オフセットであるオブジェクト値の参照アドレス
- var4:変更する値
- var5:現在のオブジェクトの値をvar5と比較します。同じ場合は、var5 + var4に更新し、trueを返します。異なる場合は、更新が完了するまで値の比較を続けます。
次の図は、AtomicIntegerが同期せずにスレッドセーフを実現できる理由を示しています
- AtomicIntegerの値の元の値は0です。つまり、メインメモリのAtomicIntegerの値は0です。JMMモデルによると、AスレッドとBスレッドはそれぞれvalue = 0のコピーを保持します。
- スレッドAはCPUリソースのvalue ++を取得し、getIntVolatileメソッドを介してメインメモリの値var5 = 0の値を取得し、compareAndSwapIntの比較と交換を実行し、スレッドAはメインメモリのValue値を正常に変更します。
- スレッドBもvalue ++を取得し、getIntVolatileメソッドを介してメインメモリで値var5 = 1を取得し、compareAndSwapInt行を比較して交換し、それが同じでないことを検出してからfalseを返し、次のループに入り、取った値とメインメモリの値が同じである場合、書き込みの上書きの問題を回避するために、つまりアトミック操作を維持するために、更新する価値があります
2.4CASのデメリット
2.4.1長期回転はリソースを消費します
スピンはcasの操作サイクルです。スレッドが特に不運な場合、取得した値が他のスレッドによって変更されるたびに、成功するまでスピン比較が続行されます。このプロセスでは、CPUオーバーヘッドが非常に高くなります。 、それを避けるようにしてください。
2.4.2共有変数のアトミック操作のみを保証できます
2.4.3ABA問題
CASアルゴリズムの重要な前提は、メモリ内の特定の瞬間のデータを現在の瞬間に取り出す必要があるということです。比較して置き換えます、次に、この時差によりデータが変更されます。たとえば、スレッド1はメインメモリからAをフェッチし、次にスレッド2もメインメモリからAをフェッチし、スレッド2はいくつかの操作の後に値をBに変更し、次に2を変更します。スレッド番号が変更されます。このとき、スレッドNo.1がCAS動作を実行すると、メモリはまだAであることがわかり、No.1スレッドの動作は成功します。
つまり、スレッド2はスレッド1からは見えません。現時点ではデータに問題はありませんが、途中で問題が発生します。
2.5AtomicStampedReferenceを使用してABA問題を解決する
ABA問題は、スレッドが最初にAであるデータを取得し、最後に変更された値もAである場合、中間プロセスが不明であり、別のスレッドが中間プロセスに表示されず、操作が成功しました。
ABA問題を解決するには、バージョン番号を追加します。つまり、各スレッドは変更されるたびにバージョン番号を変更します。比較するときにバージョン番号を比較する必要があります。そうすると、ABA問題が解決されます。
AtomicStampedReferenceクラスは、ABA問題を解決するために、アトミック操作クラスで提供されます。次のコードを見てください。
まず、ABA問題の生成をシミュレートします。
private static AtomicReference<Character> atomicReference = new AtomicReference<>('A');
public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet('A', 'B');
atomicReference.compareAndSet('B','A');
},"t1").start();
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
e.printStackTrace();}
System.out.println(atomicReference.compareAndSet('A', 'C') + "\t" + atomicReference.get());
},"t2").start();
}
t2スレッドは、Aの値をCに変更するように正常に変更されています。AtomicStampedReference操作クラスを使用してABA問題を解決する方法を見てみましょう。
private static AtomicStampedReference<Character> atomicStampedReference = new AtomicStampedReference<>('A', 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "第1次版本号" + stamp);
//睡眠1秒保证线程t1拿到当前的版本号
try {
TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {
e.printStackTrace();}
atomicStampedReference.compareAndSet('A', 'B', atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "第2次版本号" + atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet('B', 'A', atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
System.out.println(Thread.currentThread().getName() + "第3次版本号" + atomicStampedReference.getStamp());
},"t3").start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + "第1次版本号" + stamp);
//睡眠3秒保证线程t3执行完毕
try {
TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {
e.printStackTrace();}
boolean res = atomicStampedReference.compareAndSet('A', 'C', stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + "\t修改成功与否" + res);
System.out.println(Thread.currentThread().getName() + "\t当前实际版本号" + atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName() + "\t当前实际最新值" + atomicStampedReference.getReference());
},"t4").start();
}
結果: