樂觀鎖和悲觀鎖

目錄

  1. 基本概念
  2. 實現方式(含實例)
  3. 優缺點和適用場景
  4. 面試官追問:樂觀鎖加鎖嗎?
  5. 面試官追問:CAS有哪些缺點?
  6. 總結

一、基本概念

樂觀鎖和悲觀鎖是兩種思想,用于解決并發場景下的數據競爭問題。

樂觀鎖:樂觀鎖在操作數據時非常樂觀,認為別人不會同時修改數據。因此樂觀鎖不會上鎖,只是在執行更新的時候判斷一下在此期間別人是否修改了數據:如果別人修改了數據則放棄操作,否則執行操作。

悲觀鎖:悲觀鎖在操作數據時比較悲觀,認為別人會同時修改數據。因此操作數據時直接把數據鎖住,直到操作完成后才會釋放鎖;上鎖期間其他人不能修改數據。

二、實現方式(含實例)

在說明實現方式之前,需要明確:樂觀鎖和悲觀鎖是兩種思想,它們的使用是非常廣泛的,不局限于某種編程語言或數據庫。

悲觀鎖的實現方式是加鎖,加鎖既可以是對代碼塊加鎖(如Java的synchronized關鍵字),也可以是對數據加鎖(如MySQL中的排它鎖)。

樂觀鎖的實現方式主要有兩種CAS機制和版本號機制
下面詳細介紹。

2.1、CAS(Compare And Swap)

??CAS操作包括了3個操作數:
??需要讀寫的內存位置(V) 進行比較的預期值(A) 擬寫入的新值(B)

??CAS操作邏輯如下:
??如果內存位置V的值等于預期的A值,則將該位置更新為新值B,否則不進行任何操作。
??許多CAS的操作是自旋的:如果操作不成功,會一直重試,直到操作成功為止。

??這里引出一個新的問題,既然CAS包含了Compare和Swap兩個操作,它又如何保證原子性呢?答案是:CAS是由CPU支持的原子操作,其原子性是在硬件層面進行保證的。

??下面以Java中的自增操作(i++)為例,看一下悲觀鎖和CAS分別是如何保證線程安全的。
在Java中自增操作不是原子操作,它實際上包含三個獨立的操作:
讀取i值; 加1; 將新值寫回i

因此,如果并發執行自增操作,可能導致計算結果的不準確。在下面的代碼示例中:value1沒有進行任何線程安全方面的保護,value2使用了樂觀鎖(CAS),value3使用了悲觀鎖(synchronized)。

運行程序,使用1000個線程同時對value1、value2和value3進行自增操作,可以發現:value2和value3的值總是等于1000,而value1的值常常小于1000。

public class Test {

    //value1:線程不安全
    private static int value1 = 0;
    
    //value2:使用樂觀鎖
    private static AtomicInteger value2 = new AtomicInteger(0);
    
    //value3:使用悲觀鎖
    private static int value3 = 0;
    
    private static synchronized void increaseValue3(){
        value3++;
    }

    public static void main(String[] args) throws Exception {
        //開啟1000個線程,并執行自增操作
        for(int i = 0; i < 1000; ++i){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    value1++;
                    value2.getAndIncrement();
                    increaseValue3();
                }
            }).start();
        }
        
        //打印結果
        Thread.sleep(1000);
        System.out.println("線程不安全:" + value1);
        System.out.println("樂觀鎖(AtomicInteger):" + value2);
        System.out.println("悲觀鎖(synchronized):" + value3);
    }
}

[站外圖片上傳中...(image-f80179-1557143601103)]
??首先來介紹AtomicInteger。AtomicIntegerjava.util.concurrent.atomic包提供的原子類,利用CPU提供的CAS操作來保證原子性;除了AtomicInteger外,還有AtomicBoolean、AtomicLong、AtomicReference等眾多原子類。
下面看一下AtomicInteger的源碼,了解下它的自增操作incrementAndGet()是如何實現的。

[站外圖片上傳中...(image-c4727f-1557143601103)]
??在jdk1.8中,直接使用了Unsafe的getAndAddInt方法,而在jdk1.7的Unsafe中,沒有此方法?;究梢詳喽?,Unsafe新增的方法是性能提升的關鍵。
Unsafe中封裝了一些類似指針的操作,因為指針的操作是不安全的。
Unsafe的源碼
[站外圖片上傳中...(image-ecc69b-1557143601103)]
??如圖中所示:
??自增操作是自旋CAS操作:在循環中進行compareAndSwapInt,如果執行成功則退出,否則一直執行。
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
??第一個參數var1為給定的對象,var2為對象內的偏移量(其實就是一個字段到對象頭部的偏移量,通過這個偏移量可以快速定位字段),var4表示期望值,var5表示要設置的值。如果指定的字段的值等于var4,那么就會把它設置為var5.

??synchronized通過對代碼塊加鎖來保證線程安全:在同一時刻,只能有一個線程可以執行代碼塊中的代碼。synchronized是一個重量級的操作,不僅是因為加鎖需要消耗額外的資源,還因為線程狀態的切換會涉及操作系統核心態和用戶態的轉換;不過隨著JVM對鎖進行的一系列優化(如自旋鎖、輕量級鎖、鎖粗化等),synchronized的性能表現已經越來越好。

2.2、版本號機制(數據版本Version記錄機制)

??除了CAS,版本號機制也可以用來實現樂觀鎖。版本號機制的基本思路是:
讀取出數據時,將此版本號一同讀出,之后更新時,對此版本號加一。此時,將提 交數據的版本數據與數據庫表對應記錄的當前版本信息進行比對,如果提交的數據 版本號大于數據庫表當前版本號,則予以更新,否則認為是過期數據。
??需要注意的是,這里使用了版本號作為判斷數據變化的標記,實際上可以根據實際情況選用其他能夠標記數據版本的字段,如時間戳等。

假設數據庫中帳戶信息表中有一個 
version 字段,當前值為 1 ;而當前帳戶余額字段( balance )為 $100 。 
1  操作員 A 此時將其讀出( version=1 ),并從其帳戶余額中扣除 $50 
 ( $100-$50 )。 
2  在操作員 A 操作的過程中,操作員 B 也讀入此用戶信息( version=1 ),并 
   從其帳戶余額中扣除 $20 ( $100-$20 )。 
3 操作員 A 完成了修改工作,將數據版本號加一( version=2 ),連同帳戶扣 
  除后余額( balance=$50 ),提交至數據庫更新,此時由于提交數據版本大 
  于數據庫記錄當前版本,數據被更新,數據庫記錄 version 更新為 2 。 
4 操作員 B 完成了操作,也將版本號加一( version=2 )試圖向數據庫提交數 
  據( balance=$80 ),但此時比對數據庫記錄版本時發現,操作員 B 提交的 
  數據版本號為 2 ,數據庫記錄當前版本(再查一遍該記錄)也為 2 ,不滿足 “ 提交版本必須大于記 
  錄當前版本才能執行更新 “ 的樂觀鎖策略,因此,操作員 B 的提交被駁回。 
  這樣,就避免了操作員 B 用基于 version=1 的舊數據修改的結果覆蓋操作 
  員 A 的操作結果的可能。 

  從上面的例子可以看出,樂觀鎖機制避免了長事務中的數據庫加鎖開銷(操作員 A 
和操作員 B 操作過程中,都沒有對數據庫數據加鎖),大大提升了大并發量下的系 
統整體性能表現。 

三、優缺點和適用場景

樂觀鎖和悲觀鎖并沒有優劣之分,它們有各自適合的場景;下面從兩個方面進行說明。

3.1、功能限制

與悲觀鎖相比,樂觀鎖適用的場景受到了更多的限制,無論是CAS還是版本號機制。

例如,CAS只能保證單個變量操作的原子性,當涉及到多個變量時,CAS是無能為力的,而synchronized則可以通過對整個代碼塊加鎖來處理。再比如版本號機制,如果query的時候是針對表1,而update的時候是針對表2,也很難通過簡單的版本號來實現樂觀鎖。

3.2、競爭激烈程度

??如果悲觀鎖和樂觀鎖都可以使用,那么選擇就要考慮競爭的激烈程度:

當競爭不激烈 (出現并發沖突的概率小)時,樂觀鎖更有優勢,因為悲觀鎖會鎖住代碼塊或數據,其他線程無法同時訪問,影響并發,而且加鎖和釋放鎖都需要消耗額外的資源。

當競爭激烈(出現并發沖突的概率大)時,悲觀鎖更有優勢,因為樂觀鎖在執行更新時頻繁失敗,需要不斷重試,浪費CPU資源。

四、追問:樂觀鎖加鎖嗎?

??1.樂觀鎖本身是不加鎖的,只是在更新時判斷一下數據是否被其他線程更新了;AtomicInteger便是一個例子。
??2.有時樂觀鎖可能與加鎖操作合作,例如,在前述updateCoins()的例子中,MySQL在執行update時會加排它鎖。但這只是樂觀鎖與加鎖操作合作的例子,不能改變“樂觀鎖本身不加鎖”這一事實。

五、追問:CAS有哪些缺點?

下面是CAS一些不那么完美的地方:

5.1、ABA問題

假設有兩個線程——線程1和線程2,兩個線程按照順序進行以下操作: (1)線程1讀取內存中數據為A; (2)線程2將該數據修改為B; (3)線程2將該數據修改為A; (4)線程1對數據進行CAS操作
??在第(4)步中,由于內存中數據仍然為A,因此CAS操作成功,但實際上該數據已經被線程2修改過了。這就是ABA問題。

??在AtomicInteger的例子中,ABA似乎沒有什么危害。但是在某些場景下,ABA卻會帶來隱患,例如棧頂問題:一個棧的棧頂經過兩次(或多次)變化又恢復了原值,但是??赡芤寻l生了變化。
??對于ABA問題,比較有效的方案是引入版本號,內存中的值每發生一次變化,版本號都+1;在進行CAS操作時,不僅比較內存中的值,也會比較版本號,只有當二者都沒有變化時,CAS才能執行成功。Java中的AtomicStampedReference類便是使用版本號來解決ABA問題的。

5.2、高競爭下的開銷問題

??在并發沖突概率大的高競爭環境下,如果CAS一直失敗,會一直重試,CPU開銷較大。針對這個問題的一個思路是引入退出機制,如重試次數超過一定閾值后失敗退出。當然,更重要的是避免在高競爭環境下使用樂觀鎖。

5.3、功能限制

??CAS的功能是比較受限的,例如CAS只能保證單個變量(或者說單個內存值)操作的原子性,這意味著:(1)原子性不一定能保證線程安全,例如在Java中需要與volatile配合來保證線程安全;(2)當涉及到多個變量(內存值)時,CAS也無能為力。

??除此之外,CAS的實現需要硬件層面處理器的支持,在Java中普通用戶無法直接使用,只能借助atomic包下的原子類使用,靈活性受到限制。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容