也許大家已經聽說過官卡,鎖分兩種刻坊,一個叫悲觀鎖枷恕,一種稱之為樂觀鎖。Synchronized就是悲觀鎖的一種谭胚,也稱之為獨占鎖徐块,加了synchronized關鍵字的代碼基本上就只能以單線程的形式去執(zhí)行了,它會導致其他需要該資源的線程掛起灾而,直到前面的線程執(zhí)行完畢釋放所資源胡控。而另外一種樂觀鎖是一種更高效的機制,它的原理就是每次不加鎖去執(zhí)行某項操作旁趟,如果發(fā)生沖突則失敗并重試昼激,直到成功為止,其實本質上不算鎖锡搜,所以很多地方也稱之為自旋橙困。
一、并發(fā)編程中的原子性問題
在上篇JAVA并發(fā)編程(一):理解volatile關鍵字的結尾留了一個原子性的問題:
/*
* i++ 的原子性問題:i++ 的操作實際上分為三個步驟“讀-改-寫”
* int i = 10;
* i = i++; //10
*
* int temp = i;
* i = i + 1;
* i = temp;
*/
public class TestAtomicDemo {
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(ad).start();
}
}
}
class AtomicDemo implements Runnable{
private volatile int serialNumber = 0;
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
System.out.println(getSerialNumber());
}
public int getSerialNumber(){
return serialNumber++;
}
}
運行結果并不總是0-10耕餐。要想解決這個問題题禀,你可能會說加Synchronized同步鎖:加了同步鎖之后玻孟,serialNumber++操作變成了原子性操作,所以最終的輸出一定是0-9蓝谨,代碼實現(xiàn)了線程安全。
public static void main(String[] args) {
AtomicDemo ad = new AtomicDemo();
for (int i = 0; i < 10; i++) {
synchronized (TestAtomicDemo.class) {
new Thread(ad).start();
}
}
}
但是眾所周知的是Synchronized同步鎖比較耗費性能,在某些情況下并不是一個好的選擇,那有沒有什么好的辦法呢?在JDK1.5之后壹蔓,java.util.concurrent.atomic包下為我們封裝了常用的原子變量,他們底層就是使用了CAS(compare and swap)算法來保證原子性猫态,我們將上面例子中的serialNumber改為使用AtomicInteger修飾佣蓉,然后運行發(fā)現(xiàn)也可以得到正確的結果。
// private volatile int serialNumber = 0;
private AtomicInteger serialNumber = new AtomicInteger(0);
二懂鸵、什么是CAS偏螺?
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較并替換匆光。
CAS機制當中使用了3個基本操作數(shù):內存地址V套像,舊的預期值A,要修改的新值B终息。
更新一個變量的時候夺巩,只有當變量的預期值A和內存地址V當中的實際值相同時,才會將內存地址V對應的值修改為B周崭。
來看一個例子:
我們現(xiàn)在有兩個線程:
1.在內存地址V當中柳譬,存儲著值為10的變量。
2.此時線程1想要把變量的值增加1续镇。對線程1來說美澳,舊的預期值A=10,要修改的新值B=11摸航。
3.在線程1要提交更新之前制跟,另一個線程2搶先一步,把內存地址V中的變量值率先更新成了11酱虎。
4.線程1開始提交更新雨膨,首先進行A和地址V的實際值比較(Compare),發(fā)現(xiàn)A不等于V的實際值读串,提交失敗聊记。
5.線程1重新獲取內存地址V的當前值,并重新計算想要修改的新值恢暖。此時對線程1來說排监,A=11,B=12杰捂。這個重新嘗試的過程被稱為自旋社露。
6.這一次比較幸運,沒有其他線程改變地址V的值琼娘。線程1進行Compare峭弟,發(fā)現(xiàn)A和地址V的實際值是相等的。
7.線程1進行SWAP脱拼,把地址V的值替換為B瞒瘸,也就是12。
從思想上來說熄浓,Synchronized屬于悲觀鎖情臭,悲觀地認為程序中的并發(fā)情況嚴重,所以嚴防死守赌蔑。CAS屬于樂觀鎖俯在,樂觀地認為程序中的并發(fā)情況不那么嚴重,所以讓線程不斷去嘗試更新娃惯。
三跷乐、CAS的缺點
CAS機制這么巧妙,是不是在任何地方都比同步鎖要好趾浅?然而并不是這樣的愕提,CAS機制有以下幾個問題:
-
CPU開銷較大
在并發(fā)量比較高的情況下,如果許多線程反復嘗試更新某一個變量皿哨,卻又一直更新不成功浅侨,循環(huán)往復,會給CPU帶來很大的壓力证膨。 -
不能保證代碼塊的原子性
CAS機制所保證的只是一個變量的原子性操作如输,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新央勒,就不得不使用Synchronized了不见。 -
ABA問題
什么是ABA呢?簡單說就是一個值從A改成了B订歪,又從B改成了A脖祈。
1.假設內存中有一個值為A的變量,存儲在地址V當中刷晋。
2.此時有三個線程想使用CAS的方式更新這個變量值盖高,每個線程的執(zhí)行時間有略微的偏差。線程1和線程2已經獲得當前值眼虱,線程3還未獲得當前值喻奥。
3.接下來,線程1先一步執(zhí)行成功捏悬,把當前值成功從A更新為B撞蚕;同時線程2因為某種原因被阻塞住,沒有做更新操作过牙;線程3在線程1更新之后甥厦,獲得了當前值B纺铭。
4.再之后,線程2仍然處于阻塞狀態(tài)刀疙,線程3繼續(xù)執(zhí)行舶赔,成功把當前值從B更新成了A。
5.最后谦秧,線程2終于恢復了運行狀態(tài)竟纳,由于阻塞之前已經獲得了“當前值”A,并且經過compare檢測疚鲤,內存地址V中的實際值也是A锥累,所以成功把變量值A更新成了B。
6.這個過程中集歇,線程2獲取到的變量值A是一個舊值桶略,盡管和當前的實際值相同,但內存地址V中的變量已經經歷了A->B->A的改變鬼悠。
從表面看起來運行結果好像沒什么問題删性,但是結合實際情況就會出現(xiàn)問題了。比如取款時有可能發(fā)生兩個線程同時扣款成功的情況焕窝。所以蹬挺,真正要做到嚴謹?shù)腃AS機制,我們在Compare階段不僅要比較期望值A和地址V中的實際值它掂,還要比較變量的版本號是否一致巴帮。
在Java當中,AtomicStampedReference類就實現(xiàn)了用版本號做比較的CAS機制虐秋。
參考文章
本文作者: catalinaLi
本文鏈接: http://catalinali.top/2018/helloCAS/