java併發CAS
CAS你知道嗎?
併發程式中,對於count++這種操作,如果採用鎖的方式,需要先獲取到鎖,最後要釋放掉鎖,獲取不到需要等待,還需要被喚醒,這就產生了鎖競爭,和執行緒之間頻繁排程帶來的系統開銷。對於這種情況完全可以採用原子變數AtomicInteger代替,AtomicInteger底層就是使用的CAS操作。
CAS的思想是什麼?
CAS操作包含三個運算元—— 記憶體位置的值(V)、預期原值(A)和新值(B)。如果記憶體位置的值與預期原值相匹配,那麼處理器會自動將該位置更新為新值。
CAS是一條CPU的原子指令,不會造成所謂的資料不一致問題。在java 中主要是sun.misc.Unsafe這個類裡面的若干方法。
下面的的程式count++使用cas的方式保證執行緒安全,最後count的值總是150.
public class AtomicIntegerDemo { static AtomicInteger count = new AtomicInteger(); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 3; i++) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 50; i++) { try { Thread.sleep(20); //休眠20毫秒 } catch (InterruptedException e) { e.printStackTrace(); } int value = count.incrementAndGet();//++操作 System.out.println(Thread.currentThread().getName() + ":" + value); } } }, "執行緒" + i).start(); } Thread.sleep(Integer.MAX_VALUE); } }
count.incrementAndGet()分析:這裡呼叫了unsafe.getAndAddInt(this, valueOffset, 1)方法。傳入了三個引數:
1、當前AtomicInteger物件
2、當前物件value變數的記憶體地址:通過unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"))獲取
private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } }
3、加多少,這裡要加1
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
getAndAddInt方法:這裡呼叫了compareAndSwapInt方法,
引數1:物件記憶體位置
引數2:物件中變數的偏移量
引數3:變數預期值
引數4:新的值
如果compareAndSwapInt 操作失敗,重新獲取期望值,繼續迴圈,直到更新成功。
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;
}
併發包下面和AtomicInteger類似的還有
1、AtomicLong:原子Long型別
2、AtomicBoolean:原子Boolean型別
3、AtomicReference:普通物件的原子引用
下面的程式使用AtomicReference包裝了String型別,呼叫compareAndSet方法去更新atomicStr 的值為hello,只有一個執行緒可以更新成功。其他都會失敗,因為預期值和記憶體裡的值不一樣。
public class AtomicReferenceTest {
public final static AtomicReference<String> atomicStr = new AtomicReference<String>("hello");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
final int num = i;
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(Math.abs((int) (Math.random() * 100)));
} catch (InterruptedException e) {
e.printStackTrace();
}
if (atomicStr.compareAndSet("hello", "nihao")) {
System.out.println(Thread.currentThread().getName() + " success: " + atomicStr);
} else {
System.out.println(Thread.currentThread().getName() + " failure: " + atomicStr);
}
}
}, "執行緒" + i).start();
}
Thread.sleep(Integer.MAX_VALUE);
}
}
CAS引出來的ABA問題?
比如count值為0,執行緒1將count的值修改成1,執行緒2把count又修改成了0,執行緒3去更新的時候,預期值和記憶體裡的值都是0,即使count已經被修改過了,依然可以更新成功。
這時候就需要使用AtomicStampedReference,它增加了時間戳。
引數1:初始值
引數2:初始時間戳
static AtomicStampedReference<Integer> count = new AtomicStampedReference<Integer>(0, 0);
獲取時間戳
int timestamp = count.getStamp();
引數1:期望的物件引用
引數2:更新的物件引用
引數3:期望的時間戳
引數4:更新的時間戳
除了ABA問題,cas還存在哪些問題?
1、迴圈時間太長
在Java程式碼示例中我們可以看到用的是無限迴圈。如果自旋CAS長時間不成功,為給CPU帶來很大的開銷。
2、保證單個共享變數是原子操作
通過程式碼示例也可以看出,CAS只能針對單個變數;如果是多個變數那麼就要使用鎖了。
AtomicReference類來保證引用物件之間的原子性,把多個變數放在一個物件裡來進行CAS操作,倒是可以解決這個問題。