Java併發編程之CAS原理分析

Java併發編程之CAS原理分析

在併發編程中,我們經常需要處理多線程對共享資源的訪問和修改。那麼如何解決併發安全呢?


一. 解決併發安全問題的方案

最粗暴的方式就是使用 synchronized 關鍵字了,但它是一種獨佔形式的鎖,屬於悲觀鎖機制,性能會大打折扣。olatile 貌似也是一個不錯的選擇,但 volatile 只能保持變量的可見性,並不保證變量的原子性操作。

相比之下,CAS(Compare And Swap)是一種樂觀鎖機制。它不需要加鎖,是一種無鎖的原子操作。

二. 悲觀鎖和樂觀鎖
1. 悲觀鎖
對於悲觀鎖來說,它總是認爲每次訪問共享資源時會發生衝突,所以必須對每次數據操作加上鎖,以保證臨界區的程序同一時間只能有一個線程在執行。

2. 樂觀鎖
對於樂觀鎖來說,它總是假設對共享資源的訪問沒有衝突,線程可以不停地執行,無需加鎖也無需等待。一旦多個線程發生衝突,樂觀鎖通常使用一種稱爲 CAS 的技術來保證線程執行的安全性。

由於樂觀鎖假想操作中沒有鎖的存在,因此不太可能出現死鎖的情況,換句話說,樂觀鎖天生免疫死鎖。

3. 使用場景

樂觀鎖多用於“讀多寫少“的環境,避免頻繁加鎖影響性能;
悲觀鎖多用於”寫多讀少“的環境,避免頻繁失敗和重試影響性能。

三、什麼是CAS
CAS 是什麼,它的英文全稱是 Compare-And-Swap,中文叫做“比較並交換”,它是一種思想、一種算法。

CAS算法有3個基本操作數:

內存地址V
舊的預期值A
要修改的新值B
在併發場景下,各個代碼的執行順序不能確定,爲了保證併發安全,我們可以使用普通的互斥鎖,比如Java的 synchronized, ReentrantLock等。而CAS的特點是避免使用互斥鎖,當多個線程併發使用CAS更新同一個變量時,只有一個可以操作成功,其他都會失敗。而且用CAS更新失敗的線程並不會阻塞,會快速失敗並返回一個失敗的狀態,允許你再次嘗試。


   四、CAS的原理
   CAS 的思想很簡單:三個參數,一個當前內存值 V、舊的預期值 A、即將更新的值 B,當且僅當預期值 A 和內存值 V 相同時,將內存值修改爲 B 並返回 true,否則什麼都不做,並返回 false。

   CAS操作是原子的,即在同一時刻只有一個線程能夠成功執行CAS操作。

 


  CAS的基本步驟如下:

  讀取共享變量的當前值。
比較當前值與期望值。
如果相等,則使用新的值替換當前值。
如果比較失敗,則說明其他線程已經修改了共享變量,需要重新讀取當前值並重試操作。
CAS操作是一種無鎖的操作,避免了傳統鎖機制中的競爭和阻塞,從而提高了併發性能。

java.util.concurrent 包很多功能都是建立在 CAS 之上,如 ReenterLock(可重入鎖) 內部的 AQS,各種原子類,其底層都用 CAS來實現原子操作。

 

五、CAS的問題

CAS和鎖都解決了原子性問題,和鎖相比沒有阻塞、線程上下文你切換、死鎖,所以CAS要比鎖擁有更優越的性能,但是CAS同樣存在缺點。

1. 只能保證一個共享變量原子操作

CAS只能針對一個共享變量使用,如果多個共享變量就只能使用鎖了,當然如果有辦法把多個變量整成一個變量,利用CAS也不錯,例如讀寫鎖中state的高低位。

2. 自旋時間太長
當一個線程獲取鎖時失敗,不進行阻塞掛起,而是間隔一段時間再次嘗試獲取,直到成功爲止,這種循環獲取的機制被稱爲自旋鎖(spinlock)。
自旋鎖的優點:
持有鎖的線程在短時間內釋放鎖,那些等待競爭鎖的線程就不需進入阻塞狀態(無需線程上下文切換/無需用戶態與內核態切換),它們只需要等一等(自旋),等到持有鎖的線程釋放鎖之後即可獲取,這樣就避免了用戶態和內核態的切換消耗。
自旋鎖的缺點:
線程在長時間內持有鎖,等待競爭鎖的線程一直自旋,即CPU一直空轉,資源浪費在毫無意義的地方,所以一般會限制自旋次數。
最後來說自旋鎖的實現,實現自旋鎖可以基於CAS實現,先定義lockValue對象默認值1,1代表鎖資源空閒,0代表鎖資源被佔用,代碼如下:

 1 public class SpinLock {
 2     
 3     //lockValue 默認值1
 4     private AtomicInteger lockValue = new AtomicInteger(1);
 5     
 6     //自旋獲取鎖
 7     public void lock(){
 8 
 9         // 循環檢測嘗試獲取鎖
10         while (!tryLock()){
11             // 空轉
12         }
13 
14     }
15     
16     //獲取鎖
17     public boolean tryLock(){
18         // 期望值1,更新值0,更新成功返回true,更新失敗返回false
19         return lockValue.compareAndSet(1,0);
20     }
21     
22     //釋放鎖
23     public void unLock(){
24         if(!lockValue.compareAndSet(1,0)){
25             throw new RuntimeException("釋放鎖失敗");
26         }
27     }
28 
29 }

 

參考鏈接:https://segmentfault.com/a/1190000040042588

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章