《吊打面試官》系列-樂觀鎖、悲觀鎖

https://mp.weixin.qq.com/s/WtAdXvaRuBZ-SXayIKu1mA

《吊打面試官》系列-樂觀鎖、悲觀鎖


收錄於話題面試

 

數據庫

6個安全

前言

關於線程安全一提到可能就是加鎖,在面試中也是面試官百問不厭的考察點,每每能看出面試者的基本功和是否對線程安全有本身的思考。多線程

那鎖自己是怎麼去實現的呢?又有哪些加鎖的方式呢?併發

我今天就簡單聊一下樂觀鎖和悲觀鎖,他們對應的實現 CAS ,Synchronized,ReentrantLockjvm

正文

一個120斤一身黑的小夥子走了進來,看到他微微發福的面容,看來是最近疫情伙食好運動少的結果,他難道就是今天的面試官渣渣丙?ide

image.png

等等難道是他?前幾天刷B站看到的不會是他吧!!!函數

image.png

是的我已經開始把面試系列作成視頻了,之後會有各類級別的面試,從大學生到阿里P7+的面試,還有阿里,拼多多,美團,字節風格的面試我也都約好人了,就差時間了,你們能夠去B站搜:三太子敖丙觀看工具

我也很少跟你BB了,咱們直接開始好很差,你能跟我聊一下CAS麼?this

CAS(Compare And Swap 比較而且替換)是樂觀鎖的一種實現方式,是一種輕量級鎖,JUC 中不少工具類的實現就是基於 CAS 的。

CAS 是怎麼實現線程安全的?

線程在讀取數據時不進行加鎖,在準備寫回數據時,先去查詢原值,操做的時候比較原值是否修改,若未被其餘線程修改則寫回,若已被修改,則從新執行讀取流程。

舉個栗子:如今一個線程要修改數據庫的name,修改前我會先去數據庫查name的值,發現name=「帥丙」,拿到值了,咱們準備修改爲name=「三歪」,在修改以前咱們判斷一下,原來的name是否是等於「帥丙」,若是被其餘線程修改就會發現name不等於「帥丙」,咱們就不進行操做,若是原來的值仍是帥丙,咱們就把name修改成「三歪」,至此,一個流程就結束了。

有點懵?理一下停下來理一下思路。

Tip:比較+更新 總體是一個原子操做,固然這個流程仍是有問題的,我下面會提到。

他是樂觀鎖的一種實現,就是說認爲數據老是不會被更改,我是樂觀的仔,每次我都以爲你不會渣我,差很少是這個意思。

image.png

你這個栗子不錯,他存在什麼問題呢?

有,固然是有問題的,我也恰好想提到。

大家看圖發現沒,要是結果一直就一直循環了,CUP開銷是個問題,還有ABA問題和只能保證一個共享變量原子操做的問題。

你能分別介紹一下麼?

好的,我先介紹一下ABA這個問題,直接口述可能有點抽象,我畫圖解釋一下:

image.png

看到問題所在沒,我說一下順序:

  1. 線程1讀取了數據A

  2. 線程2讀取了數據A

  3. 線程2經過CAS比較,發現值是A沒錯,能夠把數據A改爲數據B

  4. 線程3讀取了數據B

  5. 線程3經過CAS比較,發現數據是B沒錯,能夠把數據B改爲了數據A

  6. 線程1經過CAS比較,發現數據仍是A沒變,就寫成了本身要改的值

懂了麼,我儘量的幼兒園化了,在這個過程當中任何線程都沒作錯什麼,可是值被改變了,線程1卻沒有辦法發現,其實這樣的狀況出現對結果自己是沒有什麼影響的,可是咱們仍是要防範,怎麼防範我下面會提到。

循環時間長開銷大的問題

是由於CAS操做長時間不成功的話,會致使一直自旋,至關於死循環了,CPU的壓力會很大。

只能保證一個共享變量的原子操做

CAS操做單個共享變量的時候能夠保證原子的操做,多個變量就不行了,JDK 5以後 AtomicReference能夠用來保證對象之間的原子性,就能夠把多個對象放入CAS中操做。

我還記得你以前說在JUC包下的原子類也是經過這個實現的,能舉個栗子麼?

那我就拿AtomicInteger舉例,他的自增函數incrementAndGet()就是這樣實現的,其中就有大量循環判斷的過程,直到符合條件才成功。

image.png

大概意思就是循環判斷給定偏移量是否等於內存中的偏移量,直到成功才退出,看到do while的循環沒。

樂觀鎖在項目開發中的實踐,有麼?

有的就好比咱們在不少訂單表,流水錶,爲了防止併發問題,就會加入CAS的校驗過程,保證了線程的安全,可是看場景使用,並非適用全部場景,他的優勢缺點都很明顯。

那開發過程當中ABA大家是怎麼保證的?

加標誌位,例如搞個自增的字段,操做一次就自增長一,或者搞個時間戳,比較時間戳的值。

舉個栗子:如今咱們去要求操做數據庫,根據CAS的原則咱們原本只須要查詢本來的值就行了,如今咱們一同查出他的標誌位版本字段vision。

以前不能防止ABA的正常修改:

update table set value = newValue where value = #{oldValue}
//oldValue就是咱們執行前查詢出來的值 

帶版本號能防止ABA的修改:

update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} 
// 判斷原來的值和版本號是否匹配,中間有別的線程修改,值可能相等,可是版本號100%不同

除了版本號,像什麼時間戳,還有JUC工具包裏面也提供了這樣的類,想要擴展的小夥伴能夠去了解一下。

聊一下悲觀鎖?

悲觀鎖從宏觀的角度講就是,他是個渣男,你認爲他每次都會渣你,因此你每次都提防着他。

咱們先聊下JVM層面的synchronized:

synchronized加鎖,synchronized 是最經常使用的線程同步手段之一,上面提到的CAS是樂觀鎖的實現,synchronized就是悲觀鎖了。

它是如何保證同一時刻只有一個線程能夠進入臨界區呢?

synchronized,表明這個方法加鎖,至關於無論哪個線程(例如線程A),運行到這個方法時,都要檢查有沒有其它線程B(或者C、 D等)正在用這個方法(或者該類的其餘同步方法),有的話要等正在使用synchronized方法的線程B(或者C 、D)運行完這個方法後再運行此線程A,沒有的話,鎖定調用者,而後直接運行。

我分別從他對對象、方法和代碼塊三方面加鎖,去介紹他怎麼保證線程安全的:

  • synchronized 對對象進行加鎖,在 JVM 中,對象在內存中分爲三塊區域:對象頭(Header)、實例數據(Instance
    Data)和對齊填充(Padding)。

  • 對象頭:咱們以Hotspot虛擬機爲例,Hotspot的對象頭主要包括兩部分數據:Mark Word(標記字段)、Klass Pointer(類型指針)。

    你能夠看到在對象頭中保存了鎖標誌位和指向 monitor 對象的起始地址,以下圖所示,右側就是對象對應的 Monitor 對象。

  • image.png

    當 Monitor 被某個線程持有後,就會處於鎖定狀態,如圖中的 Owner 部分,會指向持有 Monitor 對象的線程。

    另外 Monitor 中還有兩個隊列分別是EntryList和WaitList,主要是用來存放進入及等待獲取鎖的線程。

    若是線程進入,則獲得當前對象鎖,那麼別的線程在該類全部對象上的任何操做都不能進行。

    • Mark Word:默認存儲對象的HashCode,分代年齡和鎖標誌位信息。它會根據對象的狀態複用本身的存儲空間,也就是說在運行期間Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化。

    • Klass Point:對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。

在對象級使用鎖一般是一種比較粗糙的方法,爲何要將整個對象都上鎖,而不容許其餘線程短暫地使用對象中其餘同步方法來訪問共享資源?

若是一個對象擁有多個資源,就不須要只爲了讓一個線程使用其中一部分資源,就將全部線程都鎖在外面。

因爲每一個對象都有鎖,能夠以下所示使用虛擬對象來上鎖:

   class FineGrainLock{
   MyMemberClassx,y;
   Object xlock = new Object(), ylock = newObject();
   public void foo(){
       synchronized(xlock){
       //accessxhere
        }
       //dosomethinghere-butdon'tusesharedresources
        synchronized(ylock){
        //accessyhere
        }
   }
      public void bar(){
        synchronized(this){
           //accessbothxandyhere
       }
      //dosomethinghere-butdon'tusesharedresources
      }
  }
  • synchronized 應用在方法上時,在字節碼中是經過方法的 ACC_SYNCHRONIZED 標誌來實現的。

    我反編譯了一小段代碼,咱們能夠看一下我加鎖了一個方法,在字節碼長啥樣,flags字段矚目:

    synchronized void test();
      descriptor: ()V
      flags: ACC_SYNCHRONIZED
      Code:
        stack=0, locals=1, args_size=1
           0return
        LineNumberTable:
          line 70
        LocalVariableTable:
          Start  Length  Slot  Name   Signature
              0       1     0  this   Ljvm/ClassCompile;

    反正其餘線程進這個方法就看看是否有這個標誌位,有就表明有別的仔擁有了他,你就別碰了。

  • synchronized 應用在同步塊上時,在字節碼中是經過 monitorenter 和 monitorexit 實現的。

    每一個對象都會與一個monitor相關聯,當某個monitor被擁有以後就會被鎖住,當線程執行到monitorenter指令時,就會去嘗試得到對應的monitor。

    步驟以下:

  1. 每一個monitor維護着一個記錄着擁有次數的計數器。未被擁有的monitor的該計數器爲0,當一個線程得到monitor(執行monitorenter)後,該計數器自增變爲 1 。

  • 當同一個線程再次得到該monitor的時候,計數器再次自增;

  • 當不一樣線程想要得到該monitor的時候,就會被阻塞。

當同一個線程釋放 monitor(執行monitorexit指令)的時候,計數器再自減。

當計數器爲0的時候,monitor將被釋放,其餘線程即可以得到monitor。

一樣看一下反編譯後的一段鎖定代碼塊的結果:

public void syncTask();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=3, locals=3, args_size=1
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter  //注意此處,進入同步方法
       4: aload_0
       5: dup
       6: getfield      #2             // Field i:I
       9: iconst_1
      10: iadd
      11: putfield      #2            // Field i:I
      14: aload_1
      15: monitorexit   //注意此處,退出同步方法
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit //注意此處,退出同步方法
      22: aload_2
      23: athrow
      24return
    Exception table:
    //省略其餘字節碼.......

小結:

同步方法和同步代碼塊底層都是經過monitor來實現同步的。

二者的區別:同步方式是經過方法中的access_flags中設置ACC_SYNCHRONIZED標誌來實現,同步代碼塊是經過monitorenter和monitorexit來實現。

咱們知道了每一個對象都與一個monitor相關聯,而monitor能夠被線程擁有或釋放。

相關文章
相關標籤/搜索