看完你就知道的樂觀鎖和悲觀鎖

来源:https://www.cnblogs.com/cxuanBlog/archive/2019/09/27/11595526.html
-Advertisement-
Play Games

Java 鎖之樂觀鎖和悲觀鎖 [TOC] Java 按照鎖的實現分為樂觀鎖和悲觀鎖,樂觀鎖和悲觀鎖並不是一種真實存在的鎖,而是一種設計思想,樂觀鎖和悲觀鎖對於理解 Java 多線程和資料庫來說至關重要,那麼本篇文章就來詳細探討一下這兩種鎖的概念以及實現方式。 悲觀鎖 是一種悲觀思想,它總認為最壞的情 ...


目錄

Java 鎖之樂觀鎖和悲觀鎖

Java 按照鎖的實現分為樂觀鎖和悲觀鎖,樂觀鎖和悲觀鎖並不是一種真實存在的鎖,而是一種設計思想,樂觀鎖和悲觀鎖對於理解 Java 多線程和資料庫來說至關重要,那麼本篇文章就來詳細探討一下這兩種鎖的概念以及實現方式。

悲觀鎖

悲觀鎖是一種悲觀思想,它總認為最壞的情況可能會出現,它認為數據很可能會被其他人所修改,所以悲觀鎖在持有數據的時候總會把資源 或者 數據 鎖住,這樣其他線程想要請求這個資源的時候就會阻塞,直到等到悲觀鎖把資源釋放為止。傳統的關係型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。悲觀鎖的實現往往依靠資料庫本身的鎖功能實現。

Java 中的 SynchronizedReentrantLock 等獨占鎖(排他鎖)也是一種悲觀鎖思想的實現,因為 Synchronzied 和 ReetrantLock 不管是否持有資源,它都會嘗試去加鎖,生怕自己心愛的寶貝被別人拿走。

樂觀鎖

樂觀鎖的思想與悲觀鎖的思想相反,它總認為資源和數據不會被別人所修改,所以讀取不會上鎖,但是樂觀鎖在進行寫入操作的時候會判斷當前數據是否被修改過(具體如何判斷我們下麵再說)。樂觀鎖的實現方案一般來說有兩種: 版本號機制CAS實現 。樂觀鎖多適用於多度的應用類型,這樣可以提高吞吐量。

在Java中java.util.concurrent.atomic包下麵的原子變數類就是使用了樂觀鎖的一種實現方式CAS實現的。

兩種鎖的使用場景

上面介紹了兩種鎖的基本概念,並提到了兩種鎖的適用場景,一般來說,悲觀鎖不僅會對寫操作加鎖還會對讀操作加鎖,一個典型的悲觀鎖調用:

select * from student where name="cxuan" for update

這條 sql 語句從 Student 表中選取 name = "cxuan" 的記錄並對其加鎖,那麼其他寫操作再這個事務提交之前都不會對這條數據進行操作,起到了獨占和排他的作用。

悲觀鎖因為對讀寫都加鎖,所以它的性能比較低,對於現在互聯網提倡的三高(高性能、高可用、高併發)來說,悲觀鎖的實現用的越來越少了,但是一般多讀的情況下還是需要使用悲觀鎖的,因為雖然加鎖的性能比較低,但是也阻止了像樂觀鎖一樣,遇到寫不一致的情況下一直重試的時間。

相對而言,樂觀鎖用於讀多寫少的情況,即很少發生衝突的場景,這樣可以省去鎖的開銷,增加系統的吞吐量。

樂觀鎖的適用場景有很多,典型的比如說成本系統,櫃員要對一筆金額做修改,為了保證數據的準確性和實效性,使用悲觀鎖鎖住某個數據後,再遇到其他需要修改數據的操作,那麼此操作就無法完成金額的修改,對產品來說是災難性的一刻,使用樂觀鎖的版本號機制能夠解決這個問題,我們下麵說。

樂觀鎖的實現方式

樂觀鎖一般有兩種實現方式:採用版本號機制CAS(Compare-and-Swap,即比較並替換)演算法實現。

版本號機制

版本號機制是在數據表中加上一個 version 欄位來實現的,表示數據被修改的次數,當執行寫操作並且寫入成功後,version = version + 1,當線程A要更新數據時,在讀取數據的同時也會讀取 version 值,在提交更新時,若剛纔讀取到的 version 值為當前資料庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

我們以上面的金融系統為例,來簡述一下這個過程。

file

  • 成本系統中有一個數據表,表中有兩個欄位分別是 金額version,金額的屬性是能夠實時變化,而 version 表示的是金額每次發生變化的版本,一般的策略是,當金額發生改變時,version 採用遞增的策略每次都在上一個版本號的基礎上 + 1。
  • 在瞭解了基本情況和基本信息之後,我們來看一下這個過程:公司收到回款後,需要把這筆錢放在金庫中,假如金庫中存有100 元錢
    • 下麵開啟事務一:當男櫃員執行回款寫入操作前,他會先查看(讀)一下金庫中還有多少錢,此時讀到金庫中有 100 元,可以執行寫操作,並把資料庫中的錢更新為 120 元,提交事務,金庫中的錢由 100 -> 120,version的版本號由 0 -> 1。
    • 開啟事務二:女櫃員收到給員工發工資的請求後,需要先執行讀請求,查看金庫中的錢還有多少,此時的版本號是多少,然後從金庫中取出員工的工資進行發放,提交事務,成功後版本 + 1,此時版本由 1 -> 2。

上面兩種情況是最樂觀的情況,上面的兩個事務都是順序執行的,也就是事務一和事務二互不幹擾,那麼事務要並行執行會如何呢?

file

  • 事務一開啟,男櫃員先執行讀操作,取出金額和版本號,執行寫操作

    begin
    update 表 set 金額 = 120,version = version + 1 where 金額 = 100 and version = 0

    此時金額改為 120,版本號為1,事務還沒有提交

    事務二開啟,女櫃員先執行讀操作,取出金額和版本號,執行寫操作

    begin
    update 表 set 金額 = 50,version = version + 1 where 金額 = 100 and version = 0

    此時金額改為 50,版本號變為 1,事務未提交

    現在提交事務一,金額改為 120,版本變為1,提交事務。理想情況下應該變為 金額 = 50,版本號 = 2,但是實際上事務二 的更新是建立在金額為 100 和 版本號為 0 的基礎上的,所以事務二不會提交成功,應該重新讀取金額和版本號,再次進行寫操作。

    這樣,就避免了女櫃員 用基於 version=0 的舊數據修改的結果覆蓋男操作員操作結果的可能。

CAS 演算法

先來看一道經典的併發執行 1000次遞增和遞減後的問題:

public class Counter {

    int count = 0;

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public void add(){
        count += 1;
    }

    public void dec(){
        count -= 1;
    }
}
public class Consumer extends Thread{

    Counter counter;

    public Consumer(Counter counter){
        this.counter = counter;
    }


    @Override
    public void run() {
        for(int j = 0;j < Test.LOOP;j++){
            counter.dec();
        }
    }
}

public class Producer extends Thread{

    Counter counter;

    public Producer(Counter counter){
        this.counter = counter;
    }

    @Override
    public void run() {
        for(int i = 0;i < Test.LOOP;++i){
            counter.add();
        }
    }
}

public class Test {

    final static int LOOP = 1000;

    public static void main(String[] args) throws InterruptedException {

        Counter counter = new Counter();
        Producer producer = new Producer(counter);
        Consumer consumer = new Consumer(counter);

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();

        System.out.println(counter.getCount());

    }
}

多次測試的結果都不為 0,也就是說出現了併發後數據不一致的問題,原因是 count -= 1 和 count += 1 都是非原子性操作,它們的執行步驟分為三步:

  • 從記憶體中讀取 count 的值,把它放入寄存器中
  • 執行 + 1 或者 - 1 操作
  • 執行完成的結果再複製到記憶體中

如果要把證它們的原子性,必須進行加鎖,使用 Synchronzied 或者 ReentrantLock,我們前面介紹它們是悲觀鎖的實現,我們現在討論的是樂觀鎖,那麼用哪種方式保證它們的原子性呢?請繼續往下看

CAS 即 compare and swap(比較與交換),是一種有名的無鎖演算法。即不使用鎖的情況下實現多線程之間的變數同步,也就是在沒有線程被阻塞的情況下實現變數的同步,所以也叫非阻塞同步(Non-blocking Synchronization

CAS 中涉及三個要素:

  • 需要讀寫的記憶體值 V
  • 進行比較的值 A
  • 擬寫入的新值 B

當且僅當預期值A和記憶體值V相同時,將記憶體值V修改為B,否則什麼都不做。

JAVA對CAS的支持:在JDK1.5 中新添加 java.util.concurrent (J.U.C) 就是建立在 CAS 之上的。對於 synchronized 這種阻塞演算法,CAS是非阻塞演算法的一種實現。所以J.U.C在性能上有了很大的提升。

我們以 java.util.concurrent 中的AtomicInteger 為例,看一下在不用鎖的情況下是如何保證線程安全的

public class AtomicCounter {

    private AtomicInteger integer = new AtomicInteger();

    public AtomicInteger getInteger() {
        return integer;
    }

    public void setInteger(AtomicInteger integer) {
        this.integer = integer;
    }

    public void increment(){
        integer.incrementAndGet();
    }

    public void decrement(){
        integer.decrementAndGet();
    }

}

public class AtomicProducer extends Thread{

    private AtomicCounter atomicCounter;

    public AtomicProducer(AtomicCounter atomicCounter){
        this.atomicCounter = atomicCounter;
    }

    @Override
    public void run() {
        for(int j = 0; j < AtomicTest.LOOP; j++) {
            System.out.println("producer : " + atomicCounter.getInteger());
            atomicCounter.increment();
        }
    }
}

public class AtomicConsumer extends Thread{

    private AtomicCounter atomicCounter;

    public AtomicConsumer(AtomicCounter atomicCounter){
        this.atomicCounter = atomicCounter;
    }

    @Override
    public void run() {
        for(int j = 0; j < AtomicTest.LOOP; j++) {
            System.out.println("consumer : " + atomicCounter.getInteger());
            atomicCounter.decrement();
        }
    }
}

public class AtomicTest {

    final static int LOOP = 10000;

    public static void main(String[] args) throws InterruptedException {

        AtomicCounter counter = new AtomicCounter();
        AtomicProducer producer = new AtomicProducer(counter);
        AtomicConsumer consumer = new AtomicConsumer(counter);

        producer.start();
        consumer.start();

        producer.join();
        consumer.join();

        System.out.println(counter.getInteger());

    }
}

經測試可得,不管迴圈多少次最後的結果都是0,也就是多線程並行的情況下,使用 AtomicInteger 可以保證線程安全性。 incrementAndGet 和 decrementAndGet 都是原子性操作。本篇文章暫不探討它們的實現方式。

樂觀鎖的缺點

任何事情都是有利也有弊,軟體行業沒有完美的解決方案只有最優的解決方案,所以樂觀鎖也有它的弱點和缺陷:

ABA 問題

ABA 問題說的是,如果一個變數第一次讀取的值是 A,準備好需要對 A 進行寫操作的時候,發現值還是 A,那麼這種情況下,能認為 A 的值沒有被改變過嗎?可以是由 A -> B -> A 的這種情況,但是 AtomicInteger 卻不會這麼認為,它只相信它看到的,它看到的是什麼就是什麼。

JDK 1.5 以後的 AtomicStampedReference類就提供了此種能力,其中的 compareAndSet 方法就是首先檢查當前引用是否等於預期引用,並且當前標誌是否等於預期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設置為給定的更新值。

也可以採用CAS的一個變種DCAS來解決這個問題。
DCAS,是對於每一個V增加一個引用的表示修改次數的標記符。對於每個V,如果引用修改了一次,這個計數器就加1。然後再這個變數需要update的時候,就同時檢查變數的值和計數器的值。

迴圈開銷大

我們知道樂觀鎖在進行寫操作的時候會判斷是否能夠寫入成功,如果寫入不成功將觸發等待 -> 重試機制,這種情況是一個自旋鎖,簡單來說就是適用於短期內獲取不到,進行等待重試的鎖,它不適用於長期獲取不到鎖的情況,另外,自旋迴圈對於性能開銷比較大。

CAS與synchronized的使用情景

簡單的來說 CAS 適用於寫比較少的情況下(多讀場景,衝突一般較少),synchronized 適用於寫比較多的情況下(多寫場景,衝突一般較多)

  • 對於資源競爭較少(線程衝突較輕)的情況,使用 synchronized 同步鎖進行線程阻塞和喚醒切換以及用戶態內核態間的切換操作額外浪費消耗 cpu 資源;而 CAS 基於硬體實現,不需要進入內核,不需要切換線程,操作自旋幾率較少,因此可以獲得更高的性能。
  • 對於資源競爭嚴重(線程衝突嚴重)的情況,CAS 自旋的概率會比較大,從而浪費更多的 CPU 資源,效率低於 synchronized。

補充: Java併發編程這個領域中 synchronized 關鍵字一直都是元老級的角色,很久之前很多人都會稱它為 “重量級鎖” 。但是,在JavaSE 1.6之後進行了主要包括為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的 偏向鎖 和 輕量級鎖 以及其它各種優化之後變得在某些情況下並不是那麼重了。synchronized 的底層實現主要依靠 Lock-Free 的隊列,基本思路是 自旋後阻塞,競爭切換後繼續競爭鎖,稍微犧牲了公平性,但獲得了高吞吐量。線上程衝突較少的情況下,可以獲得和 CAS 類似的性能;而線程衝突嚴重的情況下,性能遠高於CAS。

歡迎關註我本人的公眾號,公號回覆002有你想要的一切
file

相關參考:

Java 多線程之悲觀鎖與樂觀鎖

https://baike.baidu.com/item/悲觀鎖


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • vuex(vue狀態管理) 1.先安裝vuex npm install vuex --save 2.在項目的src目錄下創建store目錄,並且新建index.js文件,然後創建vuex實例,引入vue和vuex,創建Vuex.Store實例保存到store ,最後export 導出store 3. ...
  • 前言 的確,有些標題黨了。起因是微信群里,有哥們問我,你是怎麼學習前端的呢?能不能共用一下學習方法。一句話也挺觸動我的,我真的不算是什麼大佬,對於學習前端知識,我也不能說是掌握了什麼捷徑。當然,我個人的學習方法這篇文章已經在寫了,預計這周末會在我個人公眾號發佈。而在此之前,我想展(gong)示(xi ...
  • Navigator 對象屬性 可以在Navigator對象上使用以下屬性: 屬性描述 appCodeName 返回瀏覽器的代碼名稱 appName 返回瀏覽器的名稱 appVersion 返回瀏覽器的版本信息 cookieEnabled 確定是否在瀏覽器中啟用了cookie geolocation ...
  • RESTful 是最流行的 API 設計規範,但是什麼是 RESTful?表現層狀態轉移,這恐怕誰都看不懂。本文用最通俗的說法,講明白什麼是 RESTful。 ...
  • 概述 簡單介紹一下七大設計原則: 1. 開閉原則 :是所有面向對象設計的核心,對擴展開放,對修改關閉 2. 依賴倒置原則 :針對介面編程,依賴於抽象而不依賴於具體 3. 單一職責原則 :一個介面只負責一件事情,只能有一個原因導致類變化 4. 介面隔離原則 :使用多個專門的介面,而不是使用一個總介面 ...
  • 建造者模式主要解決問題: 具備若幹成員,當其中一個成員發生變化,其它成員也隨著發生變化。 這種複雜對象的生成需要使用建造者模式來生成。 建造者設計模式的結構圖: 來源:http://c.biancheng.net/view/1354.html 例子: 街頭籃球角色創建模擬 街頭籃球: 中鋒、前鋒、後 ...
  • 一 QuerySet 可切片 使用Python 的切片語法來限制 記錄的數目 。它等同於SQL 的 和 子句。 不支持負的索引(例如 )。通常, 的切片返回一個新的 —— 它不會執行查詢。 可迭代 惰性查詢 是惰性執行的 —— 創建 不會帶來任何資料庫的訪問。你可以將過濾器保持一整天,直到 需要求值 ...
  • 程式結構設計理論(Android) 作者:鄧能財 2019年9月24日 個人簡介 姓名:鄧能財 年齡:26 畢業學校:東華理工大學 院系:理學院 專業:信息與計算科學 郵箱:[email protected] [明德厚學,愛國榮校] 本文的PPT版、以及作為案例的App項目可以從這裡下載: "程式結 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...