CAS原子操作
原子操作
所謂原子操作是指不會被執行緒排程機制打斷的操作,當某次操作一旦開始,就一直執行到結束,中間不會有任何中斷。
舉個例子:
A想要從自己的帳戶中轉1000塊錢到B的帳戶裡。那個從A開始轉帳,到轉帳結束的這一個過程,稱之為一個事務。在這個事務裡,要做如下操作:
- 從A的帳戶中減去1000塊錢。如果A的帳戶原來有3000塊錢,現在就變成2000塊錢了。
- 在B的帳戶里加1000塊錢。如果B的帳戶如果原來有2000塊錢,現在則變成3000塊錢了。
如果在A的帳戶已經減去了1000塊錢的時候,忽然發生了意外,比如停電什麼的,導致轉帳事務意外終止了,而此時B的帳戶裡還沒有增加1000塊錢。那麼,我們稱這個操作失敗了,要進行回滾。回滾就是回到事務開始之前的狀態,也就是回到A的帳戶還沒減1000塊的狀態,B的帳戶的原來的狀態。此時A的帳戶仍然有3000塊,B的帳戶仍然有2000塊。
我們把這種要麼一起成功(A帳戶成功減少1000,同時B帳戶成功增加1000),要麼一起失敗(A帳戶回到原來狀態,B帳戶也回到原來狀態)的操作叫原子性操作。
如果把一個事務可看作是一個程式,它要麼完整的被執行,要麼完全不執行。這種特性就叫原子性。
CAS
Compare And Set(或Compare And Swap),CAS是解決多執行緒並行情況下使用鎖造成效能損耗的一種機制,採用這種無鎖的原子操作可以實現執行緒安全,避免加鎖的笨重性。
CAS操作包含三個運算元:記憶體位置(V)、預期原值(A)、新值(B)。
如果記憶體位置的值與預期原值相等,那麼處理器會自動將該位置值更新為新值。否則,處理器不做任何操作。
無論哪種情況,它都會在CAS指令之前返回該位置的值。CAS有效地說明了“我認為位置V應該包含值A;如果包含該值,則將B放到這個位置;否則,不要更改該位置,只告訴我這個位置現在的值即可。”
在java中可以通過迴圈CAS的方式來實現原子操作。
CAS是實現自旋鎖的基礎,CAS 利用 CPU 指令保證了操作的原子性,以達到鎖的效果,迴圈這個指令,直到成功為止。
java提供的CAS原子操作類AtomicInteger等,核心就是CAS(CompareAndSwap)。
注意:原子操作和鎖是一樣的一種可以保證執行緒安全的方式,如何讓執行緒安全就看如何使用鎖或者如何使用原子操作CAS使用了正確的原子操作,所以保證了執行緒安全。
原子操作類
當高併發的情況下,對於基本資料型別或者引用資料型別的操作,可能會產生執行緒安全問題,為了避免多執行緒問題的處理方式一般有加鎖,但是加鎖會影響效能,所以這個時候可以考慮使用原子操作類。CAS由於是在硬體方面保證的原子性,不會鎖住當前執行緒,所以執行效率是很高的。
常見的原子操作類:
實戰
1,多執行緒累加一個較大的數值,比較原子操作類和加鎖各自的耗時。
public class AtomicIntegerDemo {
static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) throws InterruptedException {
// test();
Synch synch = new Synch();
synch.start();
synch.join();
atomic();
// System.out.println(add());
}
//使用加鎖累加
static class Synch extends Thread {
@Override
public void run() {
//使用原子操作類統計
long startTime = System.currentTimeMillis();
int count = 1;
// synchronized (this) {
// for (int i = 0; i < 1000000000; i++) {
// count++;
// }
// }
for (int i = 0; i < 1000000000; i++) {
synchronized (this) {
count++;
}
}
long endTime = System.currentTimeMillis();
System.out.println("使用鎖累加花費的時間:" + (endTime - startTime) + "......count = " + count);
}
}
//使用原子操作類統計
private static void atomic() {
long startTime = System.currentTimeMillis();
AtomicInteger atomicInteger = new AtomicInteger(1);
for (int i = 0; i < 1000000000; i++) {
atomicInteger.incrementAndGet();
}
long endTime = System.currentTimeMillis();
System.out.println("原子操作類累加花費的時間:" + (endTime - startTime) + "......count = " + atomicInteger.get());
}
//測試基本用法
private static void test() {
AtomicInteger num = new AtomicInteger(1);
//++i
System.out.println(num.incrementAndGet());
//i++
System.out.println(num.getAndIncrement());
//CAS
System.out.println(num.compareAndSet(3, 4));
System.out.println(num.get());
}
//實現自定義的原子遞增方法
private static int add() {
for (; ; ) {
int i = atomicInteger.get();
boolean b = atomicInteger.compareAndSet(i, i + 1);
if (b) {
return atomicInteger.get();
}
}
}
}
執行結果:
2,使用AtomicInteger類的get方法和compareAndSet方法實現它的遞增方法。
public class HalfAtomicInt {
private AtomicInteger atomicI = new AtomicInteger(0);
public void increament() {
for (;;) {
int i = atomicI.get();
boolean suc = atomicI.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
public int getCount() {
return atomicI.get();
}
}
總結
好處:保證了資料的原子性,避免執行緒安全問題,替代加鎖的效能消耗。
壞處:
1,ABA問題(併發1在修改資料時,雖然還是A,但已經不是初始條件的A了,中間發生了A變B,B又變A的變化,此A已經非彼A,資料卻成功修改,可能導致錯誤,這就是CAS引發的所謂的ABA問題。 可以使用樂觀鎖的方式解決。)
2,迴圈時間長開銷大(自旋)
3,只能保證一個共享變數的原子操作(可以使用AtomicRefrence原子操作類將多個變數合併成一個物件來解決)
解決ABA問題:
AtomicMarkableReference:內部是一個boolean型別的版本號,可以記錄是否被更改過。
AtomicStampedReference:內部是一個int型別的版本號,可以記錄被更改的次數。
例如:使用AtomicStampedReference,避免ABA問題,檢視內部是int型別的版本號。
public class AtomicStampedReferenceDemo {
public static void main(String[] args) throws InterruptedException {
//設定初始化版本號是0
AtomicStampedReference<String> atomicStampedReference = new AtomicStampedReference("a1", 0);
//初始的值和版本號
String reference = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("目前的值:" + reference + "............版本號:" + stamp
+ ",修改結果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
String reference = atomicStampedReference.getReference();
System.out.println("目前的值:" + reference + "............版本號:" + atomicStampedReference.getStamp()
+ ",修改結果:" + atomicStampedReference.compareAndSet(reference, "a2", stamp, stamp + 1));
}
});
thread1.start();
thread1.join();
thread2.start();
thread2.join();
}
}
執行結果: