Java:CAS(樂觀鎖)
本文講解CAS機制,主要是因為最近準備面試題,發現這個問題在面試中出現的頻率非常的高,因此把自己學習過程中的一些理解記錄下來,希望能對大家也有幫助。
什麼是悲觀鎖、樂觀鎖?在java語言裡,總有一些名詞看語義跟本不明白是啥玩意兒,也就總有部分面試官拿著這樣的詞來忽悠面試者,以此來找優越感,其實理解清楚了,這些詞也就唬不住人了。
- 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,程式碼實現了執行緒安全。
但是Synchronized
雖然確保了執行緒的安全,但是在效能上卻不是最優的,Synchronized
關鍵字會讓沒有得到鎖資源的執行緒進入BLOCKED
狀態,而後在爭奪到鎖資源後恢復為RUNNABLE
狀態,這個過程中涉及到作業系統使用者模式和核心模式的轉換,代價比較高。
儘管Java1.6為Synchronized
做了優化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度,但是在最終轉變為重量級鎖之後,效能仍然較低。
所謂原子操作類,指的是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操作的底層實現正是利用的CAS機制,好的,我們切入到這個部落格的正點。
什麼是CAS機制
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。
CAS機制當中使用了3個基本運算元:記憶體地址V,舊的預期值A,要修改的新值B。
更新一個變數的時候,只有當變數的預期值A和記憶體地址V當中的實際值相同時,才會將記憶體地址V對應的值修改為B。
CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較並替換。
CAS機制當中使用了3個基本運算元:記憶體地址V,舊的預期值A,要修改的新值B。
更新一個變數的時候,只有當變數的預期值A和記憶體地址V當中的實際值相同時,才會將記憶體地址V對應的值修改為B。
這樣說或許有些抽象,我們來看一個例子:
1.在記憶體地址V當中,儲存著值為10的變數。
image2.此時執行緒1想要把變數的值增加1。對執行緒1來說,舊的預期值A=10,要修改的新值B=11。
image3.線上程1要提交更新之前,另一個執行緒2搶先一步,把記憶體地址V中的變數值率先更新成了11。
image4.執行緒1開始提交更新,首先進行A和地址V的實際值比較(Compare),發現A不等於V的實際值,提交失敗。
4.jpg5.執行緒1重新獲取記憶體地址V的當前值,並重新計算想要修改的新值。此時對執行緒1來說,A=11,B=12。這個重新嘗試的過程被稱為自旋。
image6.這一次比較幸運,沒有其他執行緒改變地址V的值。執行緒1進行Compare,發現A和地址V的實際值是相等的。
image7.執行緒1進行SWAP,把地址V的值替換為B,也就是12。
image從思想上來說,Synchronized屬於悲觀鎖,悲觀地認為程式中的併發情況嚴重,所以嚴防死守。CAS屬於樂觀鎖,樂觀地認為程式中的併發情況不那麼嚴重,所以讓執行緒不斷去嘗試更新。
看到上面的解釋是不是索然無味,查找了很多資料也沒完全弄明白,通過幾次驗證後,終於明白,最終可以理解成一個無阻塞多執行緒爭搶資源的模型。先上程式碼
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都會執行if=true條件,而且還不會產生執行緒髒讀髒寫,這是如何做到的了,這就用到了我們的compareAndSet(boolean expect,boolean update)方法
我們看到當Thread-1在進行操作的時候,Thread一直在進行重試機制,程式原理圖:
image
這個圖中重最要的是compareAndSet(true,false)方法要拆開成compare(true)方法和Set(false)方法理解,是compare(true)是等於true後,就馬上設定共享記憶體為false,這個時候,其它執行緒無論怎麼走都無法走到只有得到共享記憶體為true時的程式隔離方法區。
看到這裡,這種CAS機制就是完美的嗎?這個程式其實存在一個問題,不知道大家注意到沒有?
但是這種得不到狀態為true時使用遞迴演算法是很耗cpu資源的,所以一般情況下,都會有執行緒sleep。
CAS的缺點:
1.CPU開銷較大
在併發量比較高的情況下,如果許多執行緒反覆嘗試更新某一個變數,卻又一直更新不成功,迴圈往復,會給CPU帶來很大的壓力。
2.不能保證程式碼塊的原子性
CAS機制所保證的只是一個變數的原子性操作,而不能保證整個程式碼塊的原子性。比如需要保證3個變數共同進行原子性的更新,就不得不使用Synchronized了。
作者:AubreyXue
連結:https://www.jianshu.com/p/ae25eb3cfb5d
來源:簡書
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。