樂觀鎖和悲觀鎖

轉:http://www.javashuo.com/article/p-fqolgorh-bc.htmlhtml

概念:樂觀鎖和悲觀鎖是兩種思想,用於解決併發場景下的數據競爭問題。java

  • 樂觀鎖:樂觀鎖在操做數據時很是樂觀,認爲別人不會同時修改數據。所以樂觀鎖不會上鎖,只是在執行更新的時候判斷一下在此期間別人是否修改了數據:若是別人修改了數據則放棄操做,不然執行操做。
  • 悲觀鎖:悲觀鎖在操做數據時比較悲觀,認爲別人會同時修改數據。所以操做數據時直接把數據鎖住,直到操做完成後纔會釋放鎖;上鎖期間其餘人不能修改數據。

    在說明實現方式以前,須要明確:樂觀鎖和悲觀鎖是兩種思想,它們的使用是很是普遍的,不侷限於某種編程語言或數據庫。面試

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

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

    一、CAS(Compare And Swap)

    CAS操做包括了3個操做數:安全

    • 須要讀寫的內存位置(V)
    • 進行比較的預期值(A)
    • 擬寫入的新值(B)

    CAS操做邏輯以下:若是內存位置V的值等於預期的A值,則將該位置更新爲新值B,不然不進行任何操做。許多CAS的操做是自旋的:若是操做不成功,會一直重試,直到操做成功爲止。併發

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

     

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

    所以,若是併發執行自增操做,可能致使計算結果的不許確。在下面的代碼示例中:value1沒有進行任何線程安全方面的保護,value2使用了樂觀鎖(CAS),value3使用了悲觀鎖(synchronized)。運行程序,使用1000個線程同時對value一、value2和value3進行自增操做,能夠發現:value2和value3的值老是等於1000,而value1的值經常小於1000。源碼分析

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    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);
         }
    }

    首先來介紹AtomicInteger。AtomicInteger是java.util.concurrent.atomic包提供的原子類,利用CPU提供的CAS操做來保證原子性;除了AtomicInteger外,還有AtomicBoolean、AtomicLong、AtomicReference等衆多原子類。

    下面看一下AtomicInteger的源碼,瞭解下它的自增操做getAndIncrement()是如何實現的(源碼以Java7爲例,Java8有所不一樣,但思想相似)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public  class  AtomicInteger  extends  Number  implements  java.io.Serializable {
         //存儲整數值,volatile保證可視性
         private  volatile  int  value;
         //Unsafe用於實現對底層資源的訪問
         private  static  final  Unsafe unsafe = Unsafe.getUnsafe();
     
         //valueOffset是value在內存中的偏移量
         private  static  final  long  valueOffset;
         //經過Unsafe得到valueOffset
         static  {
             try  {
                 valueOffset = unsafe.objectFieldOffset(AtomicInteger. class .getDeclaredField( "value" ));
             catch  (Exception ex) {  throw  new  Error(ex); }
         }
     
         public  final  boolean  compareAndSet( int  expect,  int  update) {
             return  unsafe.compareAndSwapInt( this , valueOffset, expect, update);
         }
     
         public  final  int  getAndIncrement() {
             for  (;;) {
                 int  current = get();
                 int  next = current +  1 ;
                 if  (compareAndSet(current, next))
                     return  current;
             }
         }
    }

    源碼分析說明以下:

    (1)getAndIncrement()實現的自增操做是自旋CAS操做:在循環中進行compareAndSet,若是執行成功則退出,不然一直執行。

    (2)其中compareAndSet是CAS操做的核心,它是利用Unsafe對象實現的。

    (3)Unsafe又是何許人也呢?Unsafe是用來幫助Java訪問操做系統底層資源的類(如能夠分配內存、釋放內存),經過Unsafe,Java具備了底層操做能力,能夠提高運行效率;強大的底層資源操做能力也帶來了安全隱患(類的名字Unsafe也在提醒咱們這一點),所以正常狀況下用戶沒法使用。AtomicInteger在這裏使用了Unsafe提供的CAS功能。

    (4)valueOffset能夠理解爲value在內存中的偏移量,對應了CAS三個操做數(V/A/B)中的V;偏移量的得到也是經過Unsafe實現的。

    (5)value域的volatile修飾符:Java併發編程要保證線程安全,須要保證原子性、可視性和有序性;CAS操做能夠保證原子性,而volatile能夠保證可視性和必定程度的有序性;在AtomicInteger中,volatile和CAS一塊兒保證了線程安全性。關於volatile做用原理的說明涉及到Java內存模型(JMM),這裏不詳細展開。

    說完了AtomicInteger,再說synchronized。synchronized經過對代碼塊加鎖來保證線程安全:在同一時刻,只能有一個線程能夠執行代碼塊中的代碼。synchronized是一個重量級的操做,不只是由於加鎖須要消耗額外的資源,還由於線程狀態的切換會涉及操做系統核心態和用戶態的轉換;不過隨着JVM對鎖進行的一系列優化(如自旋鎖、輕量級鎖、鎖粗化等),synchronized的性能表現已經愈來愈好。

    二、版本號機制

    除了CAS,版本號機制也能夠用來實現樂觀鎖。版本號機制的基本思路是在數據中增長一個字段version,表示該數據的版本號,每當數據被修改,版本號加1。當某個線程查詢數據時,將該數據的版本號一塊兒查出來;當該線程更新數據時,判斷當前版本號與以前讀取的版本號是否一致,若是一致才進行操做。

    須要注意的是,這裏使用了版本號做爲判斷數據變化的標記,實際上能夠根據實際狀況選用其餘可以標記數據版本的字段,如時間戳等。

     

    下面以「更新玩家金幣數」爲例(數據庫爲MySQL,其餘數據庫同理),看看悲觀鎖和版本號機制是如何應對併發問題的。

    考慮這樣一種場景:遊戲系統須要更新玩家的金幣數,更新後的金幣數依賴於當前狀態(如金幣數、等級等),所以更新前須要先查詢玩家當前狀態。

    下面的實現方式,沒有進行任何線程安全方面的保護。若是有其餘線程在query和update之間更新了玩家的信息,會致使玩家金幣數的不許確。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Transactional
    public  void  updateCoins(Integer playerId){
         //根據player_id查詢玩家信息
         Player player = query( "select coins, level from player where player_id = {0}" , playerId);
         //根據玩家當前信息及其餘信息,計算新的金幣數
         Long newCoins = ……;
         //更新金幣數
         update( "update player set coins = {0} where player_id = {1}" , newCoins, playerId);
    }

    爲了不這個問題,悲觀鎖經過加鎖解決這個問題,代碼以下所示。在查詢玩家信息時,使用select …… for update進行查詢;該查詢語句會爲該玩家數據加上排它鎖,直到事務提交或回滾時纔會釋放排它鎖;在此期間,若是其餘線程試圖更新該玩家信息或者執行select for update,會被阻塞。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Transactional
    public  void  updateCoins(Integer playerId){
         //根據player_id查詢玩家信息(加排它鎖)
         Player player = queryForUpdate( "select coins, level from player where player_id = {0} for update" , playerId);
         //根據玩家當前信息及其餘信息,計算新的金幣數
         Long newCoins = ……;
         //更新金幣數
         update( "update player set coins = {0} where player_id = {1}" , newCoins, playerId);
    }

    版本號機制則是另外一種思路,它爲玩家信息增長一個字段:version。在初次查詢玩家信息時,同時查詢出version信息;在執行update操做時,校驗version是否發生了變化,若是version變化,則不進行更新。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Transactional
    public  void  updateCoins(Integer playerId){
         //根據player_id查詢玩家信息,包含version信息
         Player player = query( "select coins, level, version from player where player_id = {0}" , playerId);
         //根據玩家當前信息及其餘信息,計算新的金幣數
         Long newCoins = ……;
         //更新金幣數,條件中增長對version的校驗
         update( "update player set coins = {0} where player_id = {1} and version = {2}" , newCoins, playerId, player.version);
    }

    3、優缺點和適用場景

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

    一、功能限制

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

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

    二、競爭激烈程度

    若是悲觀鎖和樂觀鎖均可以使用,那麼選擇就要考慮競爭的激烈程度:

    • 當競爭不激烈 (出現併發衝突的機率小)時,樂觀鎖更有優點,由於悲觀鎖會鎖住代碼塊或數據,其餘線程沒法同時訪問,影響併發,並且加鎖和釋放鎖都須要消耗額外的資源。
    • 當競爭激烈(出現併發衝突的機率大)時,悲觀鎖更有優點,由於樂觀鎖在執行更新時頻繁失敗,須要不斷重試,浪費CPU資源。

    4、面試官追問:樂觀鎖加鎖嗎?

    筆者在面試時,曾遇到面試官如此追問。下面是我對這個問題的理解:

    (1)樂觀鎖自己是不加鎖的,只是在更新時判斷一下數據是否被其餘線程更新了;AtomicInteger即是一個例子。

    (2)有時樂觀鎖可能與加鎖操做合做,例如,在前述updateCoins()的例子中,MySQL在執行update時會加排它鎖。但這只是樂觀鎖與加鎖操做合做的例子,不能改變「樂觀鎖自己不加鎖」這一事實。

    5、面試官追問:CAS有哪些缺點?

    面試到這裏,面試官可能已經中意你了。不過面試官準備對你發起最後的進攻:你知道CAS這種實現方式有什麼缺點嗎?

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

    一、ABA問題

    假設有兩個線程——線程1和線程2,兩個線程按照順序進行如下操做:

    (1)線程1讀取內存中數據爲A;

    (2)線程2將該數據修改成B;

    (3)線程2將該數據修改成A;

    (4)線程1對數據進行CAS操做

    在第(4)步中,因爲內存中數據仍然爲A,所以CAS操做成功,但實際上該數據已經被線程2修改過了。這就是ABA問題。

    在AtomicInteger的例子中,ABA彷佛沒有什麼危害。可是在某些場景下,ABA卻會帶來隱患,例如棧頂問題:一個棧的棧頂通過兩次(或屢次)變化又恢復了原值,可是棧可能已發生了變化。

    對於ABA問題,比較有效的方案是引入版本號,內存中的值每發生一次變化,版本號都+1;在進行CAS操做時,不只比較內存中的值,也會比較版本號,只有當兩者都沒有變化時,CAS才能執行成功。Java中的AtomicStampedReference類即是使用版本號來解決ABA問題的。

    二、高競爭下的開銷問題

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

    三、功能限制

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

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

相關文章
相關標籤/搜索