本文講解CAS機制罐农,主要是因為最近準備面試題,發(fā)現(xiàn)這個問題在面試中出現(xiàn)的頻率非常的高间雀,因此把自己學習過程中的一些理解記錄下來悔详,希望能對大家也有幫助。
什么是悲觀鎖惹挟、樂觀鎖茄螃?在java語言里,總有一些名詞看語義跟本不明白是啥玩意兒连锯,也就總有部分面試官拿著這樣的詞來忽悠面試者归苍,以此來找優(yōu)越感,其實理解清楚了运怖,這些詞也就唬不住人了拼弃。
- synchronized是悲觀鎖,這種線程一旦得到鎖摇展,其他需要鎖的線程就掛起的情況就是悲觀鎖吻氧。
- CAS操作的就是樂觀鎖,每次不加鎖而是假設沒有沖突而去完成某項操作咏连,如果因為沖突失敗就重試盯孙,直到成功為止。
在進入正題之前捻勉,我們先理解下下面的代碼:
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//每個線程讓count自增100次
for (int i = 0; i < 100; i++) {
count++;
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
請問cout的輸出值是否為200镀梭?答案是否定的,因為這個程序是線程不安全的踱启,所以造成的結果count值可能小于200;
那么如何改造成線程安全的呢报账,其實我們可以使用上Synchronized
同步鎖,我們只需要在count++的位置添加同步鎖,代碼如下:
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//每個線程讓count自增100次
for (int i = 0; i < 100; i++) {
synchronized (ThreadCas.class){
count++;
}
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
加了同步鎖之后埠偿,count自增的操作變成了原子性操作透罢,所以最終的輸出一定是count=200,代碼實現(xiàn)了線程安全冠蒋。
但是Synchronized
雖然確保了線程的安全羽圃,但是在性能上卻不是最優(yōu)的,Synchronized
關鍵字會讓沒有得到鎖資源的線程進入BLOCKED
狀態(tài)抖剿,而后在爭奪到鎖資源后恢復為RUNNABLE
狀態(tài)朽寞,這個過程中涉及到操作系統(tǒng)用戶模式和內核模式的轉換识窿,代價比較高。
盡管Java1.6為Synchronized
做了優(yōu)化脑融,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度喻频,但是在最終轉變?yōu)橹亓考夋i之后,性能仍然較低肘迎。
所謂原子操作類甥温,指的是java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類妓布。例如AtomicBoolean
姻蚓,AtomicInteger
,AtomicLong
匣沼。它們分別用于Boolean
狰挡,Integer
,Long
類型的原子性操作释涛。
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (Exception e) {
e.printStackTrace();
}
//每個線程讓count自增100次
for (int i = 0; i < 100; i++) {
count.incrementAndGet();
}
}
}).start();
}
try{
Thread.sleep(2000);
}catch (Exception e){
e.printStackTrace();
}
System.out.println(count);
}
使用AtomicInteger之后圆兵,最終的輸出結果同樣可以保證是200。并且在某些情況下枢贿,代碼的性能會比Synchronized更好。
而Atomic操作的底層實現(xiàn)正是利用的CAS機制刀脏,好的局荚,我們切入到這個博客的正點。
什么是CAS機制
CAS是英文單詞Compare And Swap的縮寫愈污,翻譯過來就是比較并替換耀态。
CAS機制當中使用了3個基本操作數(shù):內存地址V,舊的預期值A暂雹,要修改的新值B首装。
更新一個變量的時候,只有當變量的預期值A和內存地址V當中的實際值相同時杭跪,才會將內存地址V對應的值修改為B仙逻。
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較并替換涧尿。
CAS機制當中使用了3個基本操作數(shù):內存地址V系奉,舊的預期值A,要修改的新值B姑廉。
更新一個變量的時候缺亮,只有當變量的預期值A和內存地址V當中的實際值相同時,才會將內存地址V對應的值修改為B桥言。
這樣說或許有些抽象萌踱,我們來看一個例子:
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ā)情況不那么嚴重,所以讓線程不斷去嘗試更新橱健。
看到上面的解釋是不是索然無味而钞,查找了很多資料也沒完全弄明白,通過幾次驗證后拘荡,終于明白臼节,最終可以理解成一個無阻塞多線程爭搶資源的模型。先上代碼
import java.util.concurrent.atomic.AtomicBoolean;
/**
* @author hrabbit
* 2018/07/16.
*/
public class AtomicBooleanTest implements Runnable {
private static AtomicBoolean flag = new AtomicBoolean(true);
public static void main(String[] args) {
AtomicBooleanTest ast = new AtomicBooleanTest();
Thread thread1 = new Thread(ast);
Thread thread = new Thread(ast);
thread1.start();
thread.start();
}
@Override
public void run() {
System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
if (flag.compareAndSet(true,false)){
System.out.println(Thread.currentThread().getName()+""+flag.get());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag.set(true);
}else{
System.out.println("重試機制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
run();
}
}
}
輸出的結果:
thread:Thread-1;flag:true
thread:Thread-0;flag:true
Thread-1false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機制thread:Thread-0;flag:false
thread:Thread-0;flag:true
Thread-0false
這里無論怎么運行,Thread-1网缝、Thread-0都會執(zhí)行if=true條件巨税,而且還不會產(chǎn)生線程臟讀臟寫,這是如何做到的了粉臊,這就用到了我們的compareAndSet(boolean expect,boolean update)方法
我們看到當Thread-1在進行操作的時候草添,Thread一直在進行重試機制,程序原理圖:
這個圖中重最要的是compareAndSet(true,false)方法要拆開成compare(true)方法和Set(false)方法理解扼仲,是compare(true)是等于true后远寸,就馬上設置共享內存為false,這個時候屠凶,其它線程無論怎么走都無法走到只有得到共享內存為true時的程序隔離方法區(qū)驰后。
看到這里,這種CAS機制就是完美的嗎矗愧?這個程序其實存在一個問題灶芝,不知道大家注意到?jīng)]有?
但是這種得不到狀態(tài)為true時使用遞歸算法是很耗cpu資源的唉韭,所以一般情況下夜涕,都會有線程sleep。
CAS的缺點:
1.CPU開銷較大
在并發(fā)量比較高的情況下属愤,如果許多線程反復嘗試更新某一個變量女器,卻又一直更新不成功,循環(huán)往復住诸,會給CPU帶來很大的壓力晓避。
2.不能保證代碼塊的原子性
CAS機制所保證的只是一個變量的原子性操作,而不能保證整個代碼塊的原子性只壳。比如需要保證3個變量共同進行原子性的更新,就不得不使用Synchronized了暑塑。