CAS
首先昼扛,先說下悲觀鎖和樂觀鎖,悲觀鎖等浊,每次操作都先上鎖腮郊,比如獨(dú)占鎖synchronized就是一種悲觀鎖;樂觀鎖就是每次不加鎖而是假設(shè)沒有沖突而去完成某項(xiàng)操作筹燕,如果因?yàn)闆_突失敗就重試轧飞,直到成功為止。樂觀鎖所用到的機(jī)制就是CAS撒踪。
CAS:compare and swap
CAS包括三個值:內(nèi)存地址过咬、期待原值、新值制妄。即當(dāng)賦值的時候掸绞,首先compare,即對比目前的值和期待原值是否一樣耕捞,如果一樣衔掸,則更新原值,否則俺抽,不更新原值敞映,重新獲取。
看一個例子凌埂。
例子
public class Counter {
private AtomicInteger mAtomicInteger = new AtomicInteger(0);
private int i = 0;
public static void main(String []args) {
final Counter cas = new Counter();
List<Thread> ts = new ArrayList<>(600);
for(int j = 0; j < 100; j++) {
Thread thread = new Thread(new Runnable() {
public void run() {
for(int i = 0; i < 1000; i++) {
cas.count();
cas.safeCount();
}
}
});
ts.add(thread);
}
for(Thread thread : ts) {
thread.start();
}
//等待子線程都執(zhí)行完了之后再打印結(jié)果
for(Thread thread : ts) {
try {
thread.join();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.mAtomicInteger.get());
}
private void safeCount() {
for(;;) {
int i = mAtomicInteger.get();
boolean suc = mAtomicInteger.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
private void count() {
i++;
}
}
這個是一個計(jì)數(shù)器驱显,期待結(jié)果是100000诗芜,但是瞳抓,如果直接i++會發(fā)現(xiàn),結(jié)果不是100000伏恐,原因比較好理解孩哑,修改值的時候包括三步:
1、讀取i
2翠桦、i = i + 1
3横蜒、把計(jì)算得到的i寫入
這個過程明顯不是原子操作的,所以導(dǎo)致值不對销凑。
再看使用CAS方式來實(shí)現(xiàn)的丛晌,即:
for(;;) {
int i = mAtomicInteger.get();
boolean suc = mAtomicInteger.compareAndSet(i, ++i);
if (suc) {
break;
}
}
這個非常有意思,compareAndSet傳入的一個期待原值一個新值斗幼,如果原值和期待原值不一致澎蛛,就返回false,如果一致蜕窿,就修改并返回true谋逻。如果一致不成功就不斷重試呆馁,直到成功。
所以其實(shí)這里是通過循環(huán)CAS來實(shí)現(xiàn)的不加鎖的原子操作毁兆。
ABA問題
CAS操作的是存在著一個經(jīng)典的ABA問題浙滤,這個問題可以從下面的例子引入。
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
}
}
打印結(jié)果是true气堕。
這里初始值是100纺腊,線程1把100改成101再改成100,線程2認(rèn)為100就是期待原值茎芭,所以線程2在執(zhí)行的時候摹菠,不知道1已經(jīng)執(zhí)行了。所以返回了true骗爆。但是實(shí)際上2的期待原值100已經(jīng)不是現(xiàn)在的100了次氨。
ABA問題的解決思路就是加上版本號,即從A->B->A變成1A->2B->3A摘投,這樣一來只有版本號和值相等煮寡,才認(rèn)為相等。
從Java1.5開始犀呼,java.util.concurrent.atomic包里面引入了帶標(biāo)記的AtomicStampedReference類解決了這個問題幸撕。
還是先看這個例子的更新版。
public class ABA {
private static AtomicStampedReference<Integer> atomicStampedRef = new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
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);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}
打印結(jié)果是false外臂。
這里compareAndSet方法做了兩件事坐儿,一個是先檢查當(dāng)前引用是否等于預(yù)期引用,并且檢查當(dāng)前版本號是否等于預(yù)期版本號宋光,如果相等則進(jìn)行更新貌矿。