悲觀鎖和樂觀鎖

大綱

前言

悲觀鎖和樂觀鎖是面試的高頻問題git

咱們應該有一些概念github

悲觀鎖顧名思義,就是悲觀的認爲只要不作正確的同步措施,他就必定會出現問題面試

樂觀鎖是說對於數據的同步我樂觀的認爲不採用同步措施也不會產生問題,可是若是產生了問題我就進行補救措施,好比retry算法

多線程間的同步機制主要有四種spring

  • 互斥量
  • 臨界區
  • 信號量
  • 事件

先不講這四個的區別,只要先記住,Synchronized就是使用操做系統的互斥量安全


個人全部文章同步更新與Github--Java-Notes,想了解JVM(基本更完),HashMap源碼分析,spring相關,,併發,劍指offer題解(Java版),能夠點個star。能夠看個人github主頁,天天都在更新喲。多線程

邀請您跟我一同完成 repo併發


互斥(阻塞)同步—悲觀鎖

互斥只是同步機制的其中一個手段,也是很常見的保障併發正確性的手段函數

咱們知道傳統的鎖(如synchronized或者reentrantLock)之因此被稱爲重量級鎖,就是由於他使用操做系統互斥量來實現同步源碼分析

synchronized

這是咱們相對來講最熟悉的方式,通常新手學同步的時候,咱們都是採用這個關鍵字,可是這個方式由於是使用操做系統信號量,因此相對來講效率比較低

Java中 synchronized能實現同步基礎:Java中的對象均可以做爲鎖

三種形式:

  • 普通同步方法,鎖的的對象的實例
  • 靜態同步方法,鎖的是當前類的Class對象
  • 對於同步方法塊,鎖的是括號中的配置對象

實現原理

JVM 是基於 進入和退出 monitor對象來實現方法同步和代碼塊同步

當synchronized關鍵字通過編譯後,會在同步塊的先後(同步代碼塊開始和結束或者異常的地方)分別造成 monitorentermonitorexit兩個字節碼指令。

  • 每個monitorenter一定有一個monitorexit與之對應。當執行到monitorenter指令的時候,鎖計數器加一,對應的,執行到 monitorexit ,計數器減一。當計數器爲0的時候,鎖就被釋放
  • synchronized同步塊對於同一個線程是可重入的,不會出現本身鎖死本身的狀況
  • 同步塊在已進入的線程執行完以前,會阻塞其餘線程進入訪問

同步方法規範中並無明說,可是同步方法也但是使用上面兩個指令來實現

ReentrantLock

在基本用法上,ReentrantLock和Synchronized很類似

相比Synchronized,ReentrantLock增長了以下功能:

  • 等待可中斷
    • 顧名思義,就是等待中的線程能夠選擇放棄等待,轉而作其餘事
    • 對於執行時間長的同步塊頗有幫助
  • 可實現公平鎖
    • 什麼是公平?先來後到算一個,誰先申請,誰就先拿,按照申請鎖的順序來排序獲取鎖的順序
    • 可是非公平鎖,好比Synchronized就不是,他是隨機的
    • ReentrantLock 默認也是非公平鎖,可是能夠經過帶布爾值的構造函數來實現公平鎖
  • 鎖能夠綁定多個條件
    • 指一個ReentrantLock對象能夠同時綁定多個Condition對象,
    • 在Synchronized 中,鎖對象的 wait()和notify()或notifyAll()方法能夠實現一個隱含的條件,若是要和多餘一個條件關聯的時候,就不得不額外添加一個鎖
    • ReentrantLock 無須這樣作,只須要屢次調用newCondition()方法就行

選擇

在最新的版本中,對於Synchronized 的優化很是大,性能已經和ReentrantLock差不太多,因此若是不是使用那些高級功能,仍是建議使用 synchronized。畢竟後面仍是會大力優化Synchronized

下面是JDK8下,兩者性能對比(前者是自增,後者是鏈表)

非阻塞同步—樂觀鎖

樂觀鎖不須要線程掛起等待,因此也叫非阻塞同步

版本號機制

通常在一個數據表中加一個 version字段,表示這個數據被更新的次數,當這個數據被修改一次,版本號就加一

條件

提交版本必須大於當前記錄的版本

舉個例子

我如今銀行帳戶有 10元,如今有一個version字段,版本號爲 1.

如今我A操做取出2元,我先讀入數據 version =1,而後扣除

與此同時,B操做也要取出1元,讀入數據 version =1,而後扣除

這個時候,A操做完成,上傳數據,版本號加一,version=2,這個版本大於當前的記錄值 1,因此更新操做完成

這個時候,B操做也完成了,也要更新,他的版本號加一,version=2,而後更新的時候發現這個版本號和當前記錄的版本號相同,不知足提交版本號必須大於當前記錄的版本號的條件,不容許更新

這個時候,B操做就要從新讀入再重複以前的步驟

經過這樣的方法,咱們就保證了B操做不會將A操做所更新的值覆蓋,保證了數據的同步

CAS算法

CAS算法是基於硬件的發展產生的。爲何呢,由於咱們須要這兩個原子步驟

  • 操做
  • 衝突檢測

可是咱們如何保證上面的兩個步驟是安全的?確定不能使用互斥同步,若是使用了也不是非阻塞式了。這個時候咱們就要依賴硬件指令了,讓看似不少步驟的命令,可以使用一條指令就能完成,好比:

  • 測試並設置
  • 獲取並增長
  • 交換
  • 比較並交換(CAS)
  • 加載連接/條件存儲(LL/SC)

前面的三條是很早以前就有的指令,後面的兩條是現代的處理器纔有的指令

步驟

CAS有三個數。

  • V,內存位置
  • A,舊值
  • B,新值

當且僅當V的值符合A的值的時候,處理器纔會使用B值來更新V中的值;不然他就不執行更新。(上述比較和更新是一個原子操做)。通常狀況下這個操做是自旋的,也就是會不斷嘗試,直到完成

CAS缺點

  • ABA問題
    • 由於他是要比較V中的值和A值是否相等(更準確的是V中的值是否發生變化),可是若是相等就能說明他沒有發生變化嗎?顯然是不行的,由於這個V的值可能一開始是A,後面變成了B,而後又變成了A。顯然這樣子V的值和A是相等的,可是卻發生了變化。
    • 解決方法,給他加個版本號。
      • 每次更新,版本加一。變成1A -> 2B -> 3A
      • 在JDK1.5以後,就有這個類AtomicStampedReference,而後可使用compareAndSet方法。他首先檢查當前引用是否爲預期引用,而後檢查當前標誌是否等於預期標誌
  • 循環時間長,開銷大
    • 若是CAS不成功,他會一直消耗CPU的性能
    • 若是JVM能支持處理器提供的pause指令,性能會有必定提高
      • 他能夠延遲流水線執行指令,使CPU不會消耗過多資源
      • 避免退出循環的時候,由於內存順序衝突而致使流水線被清空,從而提升性能
  • 只能保證一個共享變量的原子操做
    • 若是操做一個共享變量,使用 CAS是可行的
    • 若是操做多個變量,那就須要使用鎖或者把它整合成一個變量
      • JDK1.5以後,有AtomicReference類,能夠把多個變量放到一個對象中,從而使用CAS操做

選擇

悲觀鎖適合寫,樂觀鎖適合讀

  • 若是寫操做多,會有不少線程衝突,那麼這個時候選擇悲觀鎖;若是讀操做多,那麼線程衝突比較少,這個時候選擇樂觀鎖

緣由

  • 寫操做多,競爭資源狀況多,CAS會由於自旋而浪費CPU資源
  • 讀操做多,競爭資源狀況少
    • 使用悲觀鎖會讓線程阻塞,而且會讓線程在用戶態和內核態來回切換(能夠看我這篇文章,Java與線程)
相關文章
相關標籤/搜索