JAVA同時プログラミング(五)原子操作CAS
6981 ワード
1.CASの由来
JDK 1.5以前のJava言語はsynchronizedキーワードによる同時制御であった.synchronizedは本質的に排他ロック/独占ロック、悲観ロックである.これにより、a.マルチスレッド競合において、ロックのロック、ロックの解放は、より多くのコンテキスト切替およびスケジューリング遅延を引き起こし、パフォーマンスの問題を引き起こすという問題が発生する.b.同一時間にオブジェクトAのロックを持つスレッドは1つしかなく、他のロックが必要なスレッドはブロック状態に入る.スレッド間の操作が互いに影響しなくても.同時の場合、volatileキーワードは共有変数の可視性を保障することができるが、原子性を保証することはできない.同時制御は最終的にはロックメカニズムによって解決される.楽観的なロックが誕生した.楽観的なロックとは、ロックをかけないたびに衝突がないと仮定して操作を完了し、衝突に失敗した場合は成功するまで再試行することです.楽観的なロックのメカニズムはCASです.
2.CASとは
CASはcompare and swapの略です.比較してハードウェアを交換すると、以前からチップに同時操作までの原語が大量に組み込まれており、ハードウェア面で効率が向上しています.IntelのCPUでは、cmpxchgコマンドを使用します.CASはこれらの命令に基づいて実現される.CAS理論はjavaです.util.concurrent同時ツールパッケージ実装の礎.ここではDoug Leaにも敬意を表します.CASオペレーションには、メモリ位置(V)、予想値(A)、および新しい値(B)の3つのオペランドがあります.メモリ位置の値が予想される値と一致する場合は、その位置値を新しい値に更新します.そうでなければ、何もしません.
通常、CASを同期に使用する方法は、アドレスVから値Aを読み出し、マルチステップ計算を実行して新しい値Bを取得し、CASを使用してVの値をAからBに変更することである.Vの値が同時に変更されていない場合、CAS操作は成功します.そうでなければ失敗します.
3.CASの問題点
CASは原子操作を効率的に解決するが、CASにもいくつかの問題がある:3.1.ABA問題CAS操作修正値の場合はVアドレスの値に変化がないかチェックし、変化がなければ更新します.例えば、Vアドレスの原値はAであり、スレッド1、スレッド2は同時にVアドレスの原値Aを取得し、予想値としてCAS操作を行う.スレッド1はまずCAS操作を完了し,値をBに変更する.スレッド2はCAS操作を完了していません.スレッド2が比較置換操作を行う前に、スレッド3はまた、Vアドレスの値をBからAに戻す.その後、スレッド2はCASの比較置換動作を円滑に通過することができる:比較Vアドレスの値は確かにAに等しく、その後、値をCに変更する.スレッド2のCAS操作は順調に完了したが.しかし、確かに問題がある.スレッド2がCAS動作を行う間にVアドレスの値が変化するためである.例えば、Jamesは水を飲む前にコップの中の水が受動的であるかどうかを検査します.プロセスは、Jamesがコップを水で満たし、予想値を取ります.コップはいっぱいです.それから水を飲む時にコップがいっぱいかどうかを検査して、もしそうなら飲んでしまいます.水を受け取った後、JamesはMarkとしばらく話した.その間、他の人はJamesのコップで水を飲んで、コップの中の水をまたいっぱいにしました.CASの方法によって:Jamesは水を飲む前にコップの中の水がいっぱいであることを検査して、そこでコップの中の水を飲みました.ABA問題の解決策はバージョン番号を使うことです.変数の前にバージョン番号を追加し、変数が更新されるたびにバージョン番号を1つ追加すると、A-B-Aは1 A-2 B-3 Aになります.Java 1から5 JDKのatomicパッケージを開始すると、2つのクラスAtomicMarkableReference、AtomicStampedReferenceがABA問題を解決するために提供されます.AtomicMarkableReferenceは原子オブジェクトが修正されたかどうかを判断します.AtomicStampedReferenceは、バージョン番号変更によって原子変数が何回修正されたかを判断することができる.
栗:import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author Ryan Lee
*/
public class TestABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(() -> {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
System.out.println(Thread.currentThread().getName()+" atomicInt");
});
Thread intT2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(Thread.currentThread().getName() + " atomicInt CAS :" + c3); //true
});
intT1.start();
intT2.start();
TimeUnit.SECONDS.sleep(2);
Thread refT1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+" atomicStampedRef");
});
Thread refT2 = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
System.out.println(Thread.currentThread().getName()+" sleep atomicStampedRef : " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" sleep atomicStampedRef : " + atomicStampedRef.getStamp()); // stamp = 0
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + " atomicStampedRef CAS :" + c3);
});
refT1.start();
refT2.start();
}
}
実行結果Thread-0 atomicInt
Thread-1 atomicInt CAS :true
Thread-3 sleep atomicStampedRef : 0
Thread-2 atomicStampedRef
Thread-3 sleep atomicStampedRef : 2
Thread-3 atomicStampedRef CAS :false
AtomicStampedReferenceのcompareAndSetメソッドは、まず、現在のバージョンが予想バージョンに等しいかどうか、現在の値が予想値に等しいかどうかを確認し、すべてが等しい場合、参照とフラグの値を所定の更新値に原子的に設定することです.
3.2. サイクル時間が長くてオーバーヘッドが大きい.スピンCASが長時間失敗すると、CPUに非常に大きな実行コストがかかります.
3.3. 共有変数の原子操作は1つしか保証できません.共有変数に対して操作を行う場合,原子操作を保証するために循環CAS方式を用いることができる.しかし,複数の共有変数に対して操作する場合,CASは操作の原子性を保証できない.Java 1から5からJDKはAtomicReferenceクラスを提供して参照オブジェクト間の原子性を保証し、複数の変数を1つのオブジェクトに配置してCAS操作を行うことができます.
栗:複数の共有変数を1つのクラスにカプセル化し、AtomicReference実装のCASメソッドを呼び出すimport java.util.concurrent.atomic.AtomicReference;
/**
* @author Ryan Lee
*/
public class TestAtomicReference {
static AtomicReference countryRef = new AtomicReference();
public static void main(String[] args) {
Country country = new Country(" ", "CN");//
countryRef.set(country);
System.out.println("CAS country:"+country.getName()+country.getCode());
System.out.println("CAS countryRef:"+countryRef.get().getName()+countryRef.get().getCode());
Country otherCountry = new Country(" ", "US");//
countryRef.compareAndSet(country, otherCountry);
System.out.println("CAS country:"+country.getName()+country.getCode());
System.out.println("CAS countryRef:"+countryRef.get().getName()+countryRef.get().getCode());
}
//
static class Country {
private String name;
private String code;
public Country(String name, String code) {
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public String getCode() {
return code;
}
}
}
実行結果:CAS country: CN
CAS countryRef: CN
CAS country: CN
CAS countryRef: US
AtomicReferenceオブジェクトcountryRefは、既存のオブジェクトcountryとは独立しています.
4.原子操作類
更新基本タイプクラス:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference更新配列クラス:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray更新参照タイプ:AtomicReference,AtomicMarkableReference,AtomicStampedReference原子更新フィールドクラス:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
CASはcompare and swapの略です.比較してハードウェアを交換すると、以前からチップに同時操作までの原語が大量に組み込まれており、ハードウェア面で効率が向上しています.IntelのCPUでは、cmpxchgコマンドを使用します.CASはこれらの命令に基づいて実現される.CAS理論はjavaです.util.concurrent同時ツールパッケージ実装の礎.ここではDoug Leaにも敬意を表します.CASオペレーションには、メモリ位置(V)、予想値(A)、および新しい値(B)の3つのオペランドがあります.メモリ位置の値が予想される値と一致する場合は、その位置値を新しい値に更新します.そうでなければ、何もしません.
通常、CASを同期に使用する方法は、アドレスVから値Aを読み出し、マルチステップ計算を実行して新しい値Bを取得し、CASを使用してVの値をAからBに変更することである.Vの値が同時に変更されていない場合、CAS操作は成功します.そうでなければ失敗します.
3.CASの問題点
CASは原子操作を効率的に解決するが、CASにもいくつかの問題がある:3.1.ABA問題CAS操作修正値の場合はVアドレスの値に変化がないかチェックし、変化がなければ更新します.例えば、Vアドレスの原値はAであり、スレッド1、スレッド2は同時にVアドレスの原値Aを取得し、予想値としてCAS操作を行う.スレッド1はまずCAS操作を完了し,値をBに変更する.スレッド2はCAS操作を完了していません.スレッド2が比較置換操作を行う前に、スレッド3はまた、Vアドレスの値をBからAに戻す.その後、スレッド2はCASの比較置換動作を円滑に通過することができる:比較Vアドレスの値は確かにAに等しく、その後、値をCに変更する.スレッド2のCAS操作は順調に完了したが.しかし、確かに問題がある.スレッド2がCAS動作を行う間にVアドレスの値が変化するためである.例えば、Jamesは水を飲む前にコップの中の水が受動的であるかどうかを検査します.プロセスは、Jamesがコップを水で満たし、予想値を取ります.コップはいっぱいです.それから水を飲む時にコップがいっぱいかどうかを検査して、もしそうなら飲んでしまいます.水を受け取った後、JamesはMarkとしばらく話した.その間、他の人はJamesのコップで水を飲んで、コップの中の水をまたいっぱいにしました.CASの方法によって:Jamesは水を飲む前にコップの中の水がいっぱいであることを検査して、そこでコップの中の水を飲みました.ABA問題の解決策はバージョン番号を使うことです.変数の前にバージョン番号を追加し、変数が更新されるたびにバージョン番号を1つ追加すると、A-B-Aは1 A-2 B-3 Aになります.Java 1から5 JDKのatomicパッケージを開始すると、2つのクラスAtomicMarkableReference、AtomicStampedReferenceがABA問題を解決するために提供されます.AtomicMarkableReferenceは原子オブジェクトが修正されたかどうかを判断します.AtomicStampedReferenceは、バージョン番号変更によって原子変数が何回修正されたかを判断することができる.
栗:import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author Ryan Lee
*/
public class TestABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(() -> {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
System.out.println(Thread.currentThread().getName()+" atomicInt");
});
Thread intT2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(Thread.currentThread().getName() + " atomicInt CAS :" + c3); //true
});
intT1.start();
intT2.start();
TimeUnit.SECONDS.sleep(2);
Thread refT1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+" atomicStampedRef");
});
Thread refT2 = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
System.out.println(Thread.currentThread().getName()+" sleep atomicStampedRef : " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" sleep atomicStampedRef : " + atomicStampedRef.getStamp()); // stamp = 0
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + " atomicStampedRef CAS :" + c3);
});
refT1.start();
refT2.start();
}
}
実行結果Thread-0 atomicInt
Thread-1 atomicInt CAS :true
Thread-3 sleep atomicStampedRef : 0
Thread-2 atomicStampedRef
Thread-3 sleep atomicStampedRef : 2
Thread-3 atomicStampedRef CAS :false
AtomicStampedReferenceのcompareAndSetメソッドは、まず、現在のバージョンが予想バージョンに等しいかどうか、現在の値が予想値に等しいかどうかを確認し、すべてが等しい場合、参照とフラグの値を所定の更新値に原子的に設定することです.
3.2. サイクル時間が長くてオーバーヘッドが大きい.スピンCASが長時間失敗すると、CPUに非常に大きな実行コストがかかります.
3.3. 共有変数の原子操作は1つしか保証できません.共有変数に対して操作を行う場合,原子操作を保証するために循環CAS方式を用いることができる.しかし,複数の共有変数に対して操作する場合,CASは操作の原子性を保証できない.Java 1から5からJDKはAtomicReferenceクラスを提供して参照オブジェクト間の原子性を保証し、複数の変数を1つのオブジェクトに配置してCAS操作を行うことができます.
栗:複数の共有変数を1つのクラスにカプセル化し、AtomicReference実装のCASメソッドを呼び出すimport java.util.concurrent.atomic.AtomicReference;
/**
* @author Ryan Lee
*/
public class TestAtomicReference {
static AtomicReference countryRef = new AtomicReference();
public static void main(String[] args) {
Country country = new Country(" ", "CN");//
countryRef.set(country);
System.out.println("CAS country:"+country.getName()+country.getCode());
System.out.println("CAS countryRef:"+countryRef.get().getName()+countryRef.get().getCode());
Country otherCountry = new Country(" ", "US");//
countryRef.compareAndSet(country, otherCountry);
System.out.println("CAS country:"+country.getName()+country.getCode());
System.out.println("CAS countryRef:"+countryRef.get().getName()+countryRef.get().getCode());
}
//
static class Country {
private String name;
private String code;
public Country(String name, String code) {
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public String getCode() {
return code;
}
}
}
実行結果:CAS country: CN
CAS countryRef: CN
CAS country: CN
CAS countryRef: US
AtomicReferenceオブジェクトcountryRefは、既存のオブジェクトcountryとは独立しています.
4.原子操作類
更新基本タイプクラス:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference更新配列クラス:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray更新参照タイプ:AtomicReference,AtomicMarkableReference,AtomicStampedReference原子更新フィールドクラス:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* @author Ryan Lee
*/
public class TestABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedRef =
new AtomicStampedReference<>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(() -> {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
System.out.println(Thread.currentThread().getName()+" atomicInt");
});
Thread intT2 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(Thread.currentThread().getName() + " atomicInt CAS :" + c3); //true
});
intT1.start();
intT2.start();
TimeUnit.SECONDS.sleep(2);
Thread refT1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
System.out.println(Thread.currentThread().getName()+" atomicStampedRef");
});
Thread refT2 = new Thread(() -> {
int stamp = atomicStampedRef.getStamp();
System.out.println(Thread.currentThread().getName()+" sleep atomicStampedRef : " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" sleep atomicStampedRef : " + atomicStampedRef.getStamp()); // stamp = 0
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName() + " atomicStampedRef CAS :" + c3);
});
refT1.start();
refT2.start();
}
}
Thread-0 atomicInt
Thread-1 atomicInt CAS :true
Thread-3 sleep atomicStampedRef : 0
Thread-2 atomicStampedRef
Thread-3 sleep atomicStampedRef : 2
Thread-3 atomicStampedRef CAS :false
import java.util.concurrent.atomic.AtomicReference;
/**
* @author Ryan Lee
*/
public class TestAtomicReference {
static AtomicReference countryRef = new AtomicReference();
public static void main(String[] args) {
Country country = new Country(" ", "CN");//
countryRef.set(country);
System.out.println("CAS country:"+country.getName()+country.getCode());
System.out.println("CAS countryRef:"+countryRef.get().getName()+countryRef.get().getCode());
Country otherCountry = new Country(" ", "US");//
countryRef.compareAndSet(country, otherCountry);
System.out.println("CAS country:"+country.getName()+country.getCode());
System.out.println("CAS countryRef:"+countryRef.get().getName()+countryRef.get().getCode());
}
//
static class Country {
private String name;
private String code;
public Country(String name, String code) {
this.name = name;
this.code = code;
}
public String getName() {
return name;
}
public String getCode() {
return code;
}
}
}
CAS country: CN
CAS countryRef: CN
CAS country: CN
CAS countryRef: US
更新基本タイプクラス:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference更新配列クラス:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray更新参照タイプ:AtomicReference,AtomicMarkableReference,AtomicStampedReference原子更新フィールドクラス:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater