併發程式設計 — CAS 原理詳解

猿博園發表於2020-12-11

在JDK1.5引入的 J.U.C包中的原子類以及Lock等都是基於 volatile 關鍵結合 CAS 操作實現的,為了能夠搞明白 原子類以及 Lock鎖的原理首先要了解 volatile 原理以及 CAS原理,上篇文章我們說了volatile關鍵字,這篇我們們就聊聊 什麼是 CAS 操作。

悲觀鎖與樂觀鎖

在說CAS操作之前我們們先說一下什麼是悲觀鎖和樂觀鎖。

悲觀鎖

悲觀鎖就是總是加鎖最壞的情況,所以每次去拿資料時都認為別人會修改,所以每次操作共享資料時都會上鎖,這樣當別人需要訪問共享資料時,就必須阻塞等待,直到它拿到鎖。傳統的關係型資料庫裡面就用到了很多這種鎖機制,比如行鎖、表鎖等,都是在操作之前先加上鎖。在Java中 synchronized 和 ReentrantLock 等都是獨佔的,也就是悲觀鎖。

悲觀鎖可以理解為就是 “總有刁民想害朕”。

樂觀鎖

樂觀鎖就是總是情況最好的,每次去拿共享資料的時候認為別人不會修改,所以不會上鎖,但是在更新資料的時候會判斷一下在此期間別人是否更改了資料。一般可以通過版本號和 CAS演算法實現。樂觀鎖適用於讀多寫少的場景,這樣可以提供吞吐量

什麼是CAS

CAS 即 Compare And Swap (比較與交換),是一種有名的無鎖演算法。CAS演算法主要涉及到三個引數:

  • 主記憶體中存放的的值V,也就是執行緒共享的值。
  • 執行緒上次從記憶體中讀取的V值 A,執行緒私有的,存放線上程棧幀中,或者CPU快取中的值。
  • 需要寫入主記憶體改些V的值B,執行緒對A計算後的值。

大致流程如下所示:

 

CAS帶來的三大問題

ABA問題

因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼CAS進行檢查的時候發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面加上版本號,每次變數更新的時候把版本號加1,那麼A->B->A就會變成1A->2B->3A。從Java 1.5開始,JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前的標誌是否等於預期標誌,如果全部相等,則以原子方式將該應用和該標誌的值設定為給定的更新值。

開銷大的問題

自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷,如果JVM能支援處理器提供的pause指令,那麼效率會有一定的提升。pause指令有兩個作用:

第一,它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零;

第二,它可以避免在迴圈的時候因記憶體順序衝突(Memory Order Violation)而引起CPU流水線被清空,從而提高CPU的實行效率。

只能保證一個共享變數的原子操作

當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候可以用鎖。還有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如,有兩個共享變數i=2,j=a,合併一下ji=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用物件之前的原子性,就可以把多個變數放在一個物件裡來進行CAS操作。

總結

本篇文章首先介紹了什麼是悲觀鎖和樂觀鎖,然後介紹了 CAS 原理,CAS 就是 比較並交換,也就是在寫入時先比較 主記憶體中值是否跟之前讀取的舊值相等,如果相等就更新,如果不相等就再次讀取計算,迴圈往復。CAS 在提高效能的同時,也帶來了三大問題。明白三大問題能夠更好的使用 CAS操作,以及後續的 原子操作類。

相關文章