动态

详情 返回 返回

淺談java中的悲觀鎖,樂觀鎖以及CAS操作 - 动态 详情

瞭解volatile的同學一定知道,volatile 可以保證可見性,但是它無法保證原子性。

所謂原子性,就是一個(一系列)操作,要麼全都執行,要麼全都不執行,不能執行到中間某種狀態就結束,同時對於外界(其它)來看,要麼就是看到執行前的結果,要麼就是執行後的結果,不能看到中間狀態。
舉一個經典的例子:多線程對於全局volatile 變量的累加,(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )代碼如下:

 1 public class Main {
 2     static volatile int count = 0;
 3     static final int TOTAL = 10000;
 4 
 5     public static void main(String[] args) throws InterruptedException {
 6         Runnable r = () -> {
 7             for (int i = 0; i < TOTAL; i++) {
 8                 count++;
 9             }
10         };
11 
12         Thread t1 = new Thread(r);
13         Thread t2 = new Thread(r);
14         t1.start();
15         t2.start();
16 
17         t1.join();
18         t2.join();
19 
20         System.out.println("echo :" + count);
21     }
22 }

這個代碼的執行結果如下,多次執行也基本不會達到目標值20000

1 Connected to the target VM, address: '127.0.0.1:54088', transport: 'socket'
2 echo :13533
3 Disconnected from the target VM, address: '127.0.0.1:54088', transport: 'socket'

產生這個問題的原因是,我們在處理自增操作時,它不是原子性的。

雖然兩個線程對於這個變量的操作變化都是實時感知的,讀的都是是實時值,但是計算和回寫時可能就會出問題了。
詳細説下
A 線程 將變量x自增為1
B線程讀取1 ,B線程計算+1時,得到結果是2(注意此時2存在臨時變量中),在計算+1時,A線程已經繼續自增變量x到2甚至3,4,5,6...
B線程回寫臨時結果到變量x ,此時覆蓋了A的操作,x 又變為了2。
此時B線程的操作就是中間狀態執行期間,被其它線程併發操作了。導致回寫失敗。
那怎麼解決呢?最常規的辦法就是加併發鎖,將併發的片段同步成一個整體,(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )執行期間,不允許其它線程同步操作。
這種常規的辦法就是加鎖,這種鎖通常是指無論併發是否發生,我先加鎖,保證我在執行期間肯定不受到干擾。我們將這種時刻防護併發保護數據安全的鎖稱之為悲觀鎖
這就像是遊客進入地鐵閘機,不管有沒有其他遊客準備並行進入。閘機通道,每次只限一個人操作。

bgszj

閘機的旋轉門旋轉 (加鎖)

進入人 (數據操作)
離開閘機,進入景區 (解鎖)
像傳統的 synchronized ReentrantLock 等鎖,都是悲觀鎖。都有典型的加鎖解鎖操作。

bgs2

悲觀鎖鎖常用於競爭激烈的併發場景下。

 

除了加併發鎖還有啥辦法呢?
還可以通過狀態的變化來控制。就以我們這個例子來説。
因為出現問題的本質時因為發生了併發,我們只要判斷併發有沒有發生就可以。
如果併發沒發生,我直接操作有沒有鎖無所謂,如果併發發生了,我看下對我的影響,如果對我有影響,我就認為這次操作失敗了,重新操作試下。
我們觀察有沒有發生併發有兩個點,開始和結束點,
<1>如果在開始點觀察:其它線程有沒有也同步讀取數據。細想就發現這太難了,(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )首先你要併發的觀察所有cpu核的線程有沒有讀數據,這個挑戰太大。而且別人可能只是簡單的讀取一下不操作,或者即使你能觀察到,別的線程也可能先於你觀察就已經讀到數據了。
這顯然不可行的。
<2>其次就是觀察結束點,數據有沒有改變。別人怎麼讀無所謂。這種顯然是可以的。我們只要監控變量的值發生變法了沒有即可判斷是否發生了併發,從而判斷是否可以繼續寫。

如果線程讀取的是1,操作回寫的結果是2(新值),它就可以在回寫時,判斷下回寫要覆蓋的值是不是1(舊值)。如果是則覆蓋寫入,如果不是,則認為併發失敗,重新嘗試寫入(或進行其它失敗策略)。
這種通過判斷併發是否發生才進行操作的方式,我們稱之為樂觀鎖
樂觀併發控制一般分為三個步驟:
(1)讀取 read
(2)修改 modify (計算出目標值)
(3)校驗並提交/寫入 (Validate & Commit)

bgs2

樂觀鎖常常用於低併發的場景中。因為它避免了悲觀鎖的狀態切換,因此它的性能在低併發時更高,高併發下由於衝突較多,會導致比較次數較多,從而導致性能下降。

我們業務中最常見的樂觀鎖,一般是在數據庫層面通過where 語句來實現,
比如下邊這個語句

update status = '待支付'
from order
where status = '已下單'

當用户下單後,校驗身份

訂單狀態從初始-->(check用户身份)-->已下單-->(鎖定庫存)-->待支付
每次訂單狀態機發生正常業務狀態跳轉時,都check狀態是否是預定狀態,但是此時用户又可以併發的去操作訂單,如取消訂單,這時狀態機的正常業務狀態跳轉就要發現被併發修改了,進而失敗退出。
這就是樂觀鎖的一種典型應用場景。
像例子中這種比較變量當前值是否是預期值,如果是,就將變量值賦為心值(預期值交換為新值),如果不是則不做操作的行為
我們稱之為compare and swap 比較並交換,也就是大家常説的CAS.
CAS 是一種思路,也是樂觀鎖的一種實現方式,除此之外,還可以通過數據庫主鍵控制,數據庫版本號等方式來實現,但是本質都差不多,就是在寫入時進行原子級別的比較並寫入
java中已經通過unsafe類結合c++代碼實現了CAS的能力,但是操作不太方便,因此JUC中atomic包下提供了各種原子類,如:
AtomicBoolean、AtomicInteger、AtomicReference 等。(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )這些類可以直接用於業務代碼的各種類型的原子操作類。(Atomic原子類/unsafe類的實現和使用,我會在後邊的文章中專門講解)
但是CAS操作本身是無法直接的解決ABA問題的。
什麼是ABA問題,就是指CAS 在比較預期值時,雖然值等於預期值,但是可能已經發生併發了,
比如線程1發現變量值為A,
線程1準備將A調整為C,
此時發生了併發,其它線程將變量值調整為了B,因為某種原因調整回A
線程1CAS 寫入變量時,A仍然等於預期值,但是已經不是原來的A了,此時再發生寫入,可能會有異常或場景遺漏。
這種情況往往發生在變量值可以發生循環變化時,對於不會循環時,這個問題就不會產生。
常規的解決辦法就是加入一個新的變量,如版本號,版本號和每次的變量值時一一映射關係。這樣即使變量值循環回去,但是版本號只會遞增不會循環。
一般的數值操作,即使有ABA場景的發生也不用擔心,大部分由於最終一致性的情況,並不會對業務有什麼衝擊。只有很少的場景需要結合業務或者是對象內部變化,才會引發新的問題。

最後再説下很多人提到的CAS是無鎖麼?(防盜連接:本文首發自http://www.cnblogs.com/jilodream/ )並不是,CAS也是樂觀鎖的一種實現,也是鎖,雖然我們沒有顯示的使用,但是內部在真正實現原子操作的那個時間段內還是需要通過各種狀態、指令來控制住了併發。
比如通過數據庫實現的CAS 樂觀鎖,那麼在update時,一般會有行鎖或者表鎖。
通過atomic包下的原子類進行cas,雖然沒有直接使用鎖,但是在底層調用C++進而調用cpu指令cmpxchg時,還是通過lock 指令來鎖定內存指令或者緩存行來保證控制併發。

因此CAS 肯定是用到了鎖,但是對於應用層面的業務來説,感知不到鎖。

 

 

 

 

 

Add a new 评论

Some HTML is okay.