多線程系列(十一) -淺析併發讀寫鎖StampedLock

来源:https://www.cnblogs.com/dxflqm/p/18051186
-Advertisement-
Play Games

一、摘要 在上一篇文章中,我們講到了使用ReadWriteLock可以解決多線程同時讀,但只有一個線程能寫的問題。 如果繼續深入的分析ReadWriteLock,從鎖的角度分析,會發現它有一個潛在的問題:如果有線程正在讀數據,寫線程準備修改數據的時候,需要等待讀線程釋放鎖後才能獲取寫鎖,簡單的說就是 ...


一、摘要

在上一篇文章中,我們講到了使用ReadWriteLock可以解決多線程同時讀,但只有一個線程能寫的問題。

如果繼續深入的分析ReadWriteLock,從鎖的角度分析,會發現它有一個潛在的問題:如果有線程正在讀數據,寫線程準備修改數據的時候,需要等待讀線程釋放鎖後才能獲取寫鎖,簡單的說就是,讀的過程中不允許寫,這其實是一種悲觀的讀鎖。

為了進一步的提升程式併發執行效率,Java 8 引入了一個新的讀寫鎖:StampedLock

ReadWriteLock相比,StampedLock最大的改進點在於:在原先讀寫鎖的基礎上,新增了一種叫樂觀讀的模式。該模式並不會加鎖,因此不會阻塞線程,程式會有更高的執行效率。

什麼是樂觀鎖和悲觀鎖呢?

  • 樂觀鎖:就是樂觀的估計讀的過程中大概率不會有寫入,因此被稱為樂觀鎖
  • 悲觀鎖:指的是讀的過程中拒絕有寫入,也就是寫入必須等待

顯然樂觀鎖的併發執行效率會更高,但一旦有數據的寫入導致讀取的數據不一致,需要能檢測出來,再讀一遍就行。

下麵我們一起來瞭解一下StampedLock的用法!

二、StampedLock

StampedLock的使用方式比較簡單,只需要實例化一個StampedLock對象,然後調用對應的讀寫方法即可,它有三個核心方法如下!

  • readLock():表示讀鎖,多個線程讀不會阻塞,效果與ReadWriteLock的讀鎖模式類似
  • writeLock():表示寫鎖,同一時刻有且只有一個寫線程能獲取鎖資源,效果與ReadWriteLock的寫鎖模式類似
  • tryOptimisticRead():表示樂觀讀,並沒有加鎖,它用於非常短的讀操作,允許多個線程同時讀

其中readLock()writeLock()方法,與ReadWriteLock的效果完全一致,在此就不重覆演示了。

下麵我們來看一個tryOptimisticRead()方法的簡單使用示例。

2.1、tryOptimisticRead 方法

public class CounterDemo {

    private final StampedLock lock = new StampedLock();

    private int count;

    public void write() {
        // 1.獲取寫鎖
        long stamp = lock.writeLock();
        try {
            count++;
            // 方便演示,休眠一下
            sleep(200);
            println("獲得了寫鎖,count:" + count);
        } finally {
            // 2.釋放寫鎖
            lock.unlockWrite(stamp);
        }
    }

    public int read() {
        // 1.嘗試通過樂觀讀模式讀取數據,非阻塞
        long stamp = lock.tryOptimisticRead();
        // 2.假設x = 0,但是x可能被寫線程修改為1
        int x = count;
        // 方便演示,休眠一下
        int millis = new Random().nextInt(500);
        sleep(millis);
        println("通過樂觀讀模式讀取數據,value:" + x + ", 耗時:" + millis);
        // 3.檢查樂觀讀後是否有其他寫鎖發生
        if(!lock.validate(stamp)){
            // 4.如果有,採用悲觀讀鎖,並重新讀取數據到當前線程局部變數
            stamp = lock.readLock();
            try {
                x = count;
                println("樂觀讀後檢查到數據發生變化,獲得了讀鎖,value:" + x);
            } finally{
                // 5.釋放悲觀讀鎖
                lock.unlockRead(stamp);
            }
        }
        // 6.返回讀取的數據
        return x;
    }


    private void sleep(long millis){
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }


    private void println(String message){
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
        System.out.println(time + " 線程:" + Thread.currentThread().getName() + " " + message);
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        CounterDemo counter = new CounterDemo();
        Runnable readRunnable = new Runnable() {
            @Override
            public void run() {
                counter.read();
            }
        };
        Runnable writeRunnable = new Runnable() {
            @Override
            public void run() {
                counter.write();
            }
        };
        // 啟動3個讀線程
        for (int i = 0; i < 3; i++) {
            new Thread(readRunnable).start();
        }
        // 停頓一下
        Thread.sleep(300);
        // 啟動3個寫線程
        for (int i = 0; i < 3; i++) {
            new Thread(writeRunnable).start();
        }
    }
}

看一下運行結果:

2023-10-25 13:47:16:952 線程:Thread-0 通過樂觀讀模式讀取數據,value:0, 耗時:19
2023-10-25 13:47:17:050 線程:Thread-2 通過樂觀讀模式讀取數據,value:0, 耗時:172
2023-10-25 13:47:17:247 線程:Thread-1 通過樂觀讀模式讀取數據,value:0, 耗時:369
2023-10-25 13:47:17:382 線程:Thread-3 獲得了寫鎖,count:1
2023-10-25 13:47:17:586 線程:Thread-4 獲得了寫鎖,count:2
2023-10-25 13:47:17:788 線程:Thread-5 獲得了寫鎖,count:3
2023-10-25 13:47:17:788 線程:Thread-1 樂觀讀後檢查到數據發生變化,獲得了讀鎖,value:3

從日誌上可以分析得出,讀線程Thread-0Thread-2在啟動寫線程之前就已經執行完,因此沒有進入競爭讀鎖階段;而讀線程Thread-1因為在啟動寫線程之後才執行完,這個時候檢查到數據發生變化,因此進入讀鎖階段,保證讀取的數據是最新的。

ReadWriteLock相比,StampedLock寫入數據的加鎖過程基本類似,不同的是讀取數據。

讀取數據大致的過程如下:

  • 1.嘗試通過tryOptimisticRead()方法樂觀讀模式讀取數據,並返回版本號
  • 2.數據讀取完成後,再通過lock.validate()去驗證版本號,如果在讀取過程中沒有寫入,版本號不會變,驗證成功,直接返回結果
  • 3.如果在讀取過程中有寫入,版本號會發生變化,驗證將失敗。在失敗的時候,再通過悲觀讀鎖再次讀取數據,把讀取的最新結果返回

對於讀多寫少的場景,由於寫入的概率不高,程式在絕大部分情況下可以通過樂觀讀獲取數據,極少數情況下使用悲觀讀鎖獲取數據,併發執行效率得到了大大的提升。

樂觀鎖實際用途也非常廣泛,比如資料庫的欄位值修改,我們舉個簡單的例子。

在訂單庫存表上order_store,我們通常會增加了一個數值型版本號欄位version,每次更新order_store這個表庫存數據的時候,都將version欄位加1,同時檢查version的值是否滿足條件。

select id,... ,version
from order_store
where id = 1000
update order_store
set version = version + 1,...
where id = 1000 and version = 1

資料庫的樂觀鎖,就是查詢的時候將version查出來,更新的時候利用version欄位驗證是否一致,如果相等,說明數據沒有被修改,讀取的數據安全;如果不相等,說明數據已經被修改過,讀取的數據不安全,需要重新讀取。

這裡的version就類似於StampedLockstamp值。

2.2、tryConvertToWriteLock 方法

其次,StampedLock還提供了將悲觀讀鎖升級為寫鎖的功能,對應的核心方法是tryConvertToWriteLock()

它主要使用在if-then-update的場景,即:程式先採用讀模式,如果讀的數據滿足條件,就返回;如果讀的數據不滿足條件,再嘗試寫。

簡單示例如下:

public int readAndWrite(Integer newCount) {
    // 1.獲取讀鎖,也可以使用樂觀讀
    long stamp = lock.readLock();
    int currentValue = count;
    try {
        // 2.檢查是否讀取數據
        while (Objects.isNull(currentValue)) {
            // 3.如果沒有,嘗試升級寫鎖
            long wl = lock.tryConvertToWriteLock(stamp);
            // 4.不為 0 升級寫鎖成功
            if (wl != 0L) {
                // 重新賦值
                stamp = wl;
                count = newCount;
                currentValue = count;
                break;
            } else {
                // 5.升級失敗,釋放之前加的讀鎖並上寫鎖,通過迴圈再試
                lock.unlockRead(stamp);
                stamp = lock.writeLock();
            }
        }
    } finally {
        // 6.釋放最後加的鎖
        lock.unlock(stamp);
    }
    // 7.返回讀取的數據
    return currentValue;
}

三、小結

總結下來,與ReadWriteLock相比,StampedLock進一步把讀鎖細分為樂觀讀和悲觀讀,能進一步提升了併發執行效率。

好處是非常明顯的,系統性能得到提升,但是代價也不小,主要有以下幾點:

  • 1.代碼邏輯更加複雜,如果編程不當很容易出 bug
  • 2.StampedLock是不可重入鎖,不能在一個線程中反覆獲取同一個鎖,如果編程不當,很容易出現死鎖
  • 3.如果線程阻塞在StampedLockreadLock()或者writeLock()方法上時,此時試圖通過interrupt()方法中斷線程,會導致 CPU 飆升。因此,使用 StampedLock一定不要調用中斷操作,如果需要支持中斷功能,推薦使用可中斷的讀鎖readLockInterruptibly()或者寫鎖writeLockInterruptibly()方法。

最後,在實際的使用過程中,樂觀讀編程模型,推薦可以按照以下固定模板編寫。

public int read() {
    // 1.嘗試通過樂觀讀模式讀取數據,非阻塞
    long stamp = lock.tryOptimisticRead();
    // 2.假設x = 0,但是x可能被寫線程修改為1
    int x = count;
    // 3.檢查樂觀讀後是否有其他寫鎖發生
    if(!lock.validate(stamp)){
        // 4.如果有,採用悲觀讀鎖,並重新讀取數據到當前線程局部變數
        stamp = lock.readLock();
        try {
            x = count;
        } finally{
            // 5.釋放悲觀讀鎖
            lock.unlockRead(stamp);
        }
    }
    // 6.返回讀取的數據
    return x;
}

四、參考

1、https://www.liaoxuefeng.com/wiki/1252599548343744/1309138673991714

2、https://zhuanlan.zhihu.com/p/257868603


作者:程式員志哥
出處:pzblog.cn
資源:微信搜【程式員志哥】關註我,回覆 【技術資料】有我準備的一線程式必備電腦書籍、大廠面試資料和免費電子書。 希望可以幫助大家提升技術和能力。


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

-Advertisement-
Play Games
更多相關文章
  • 在Spring中,實例化Bean對象涉及構造方法的調用。通過分析源碼,我們瞭解到實例化的步驟和推斷構造方法的過程。當一個類只有一個構造方法時,Spring會根據具體情況決定是否使用該構造方法。如果一個類存在多個構造方法,就需要根據具體情況具體分析。 ...
  • polymorphism 靜態聯編和動態聯編 多態性(polymorphism)提供介面與具體實現之間的另一層隔離,從而將”what”和”how”分離開來。多態性改善了代碼的可讀性和組織性,同時也使創建的程式具有可擴展性,項目不僅在最初創建時期可以擴展,而且當項目在需要有新的功能時也能擴展。 c++ ...
  • 一、簡介 在 Java 多線程編程中,還有一個非常重要的設計模式,它就是:生產者和消費者模型。 這種模型可以充分發揮 cpu 的多線程特性,通過一些平衡手段能有效的提升系統整體處理數據的速度,減輕系統負載,提高程式的效率和穩定性,同時實現模塊之間的解耦。 那什麼是生產者和消費者模型呢? 簡單的說,生 ...
  • 下拉列表(下拉框)可以確保用戶僅從預先給定的選項中進行選擇,這樣不僅能減少數據輸入錯誤,還能節省時間提高效率。在MS Excel中,我們可以通過 “數據驗證” 提供的選項來創建下拉列表,但如果要在Java程式中通過代碼實現這一功能,可能需要藉助一些第三方庫。本文將分享兩種使用免費Java庫在Exce ...
  • Playwright是由微軟公司2020年初發佈的新一代自動化測試工具,相較於目前最常用的Selenium,它僅用一個API即可自動執行Chromium、Firefox、WebKit等主流瀏覽器自動化操作。 對各種開發語言也有非常好的支持。常用的NodeJs、Java、python都有支持,且有豐富 ...
  • 這是我讀大學時的Java知識點總結,還不全面,後續會逐漸增加完善。 知識點集合 實例變數 實例變數是指在類中聲明的變數,其值是針對類的每個實例而獨立存儲的。每個類的實例都有自己的一組實例變數,它們的值可以在對象創建時初始化,併在整個對象的生命周期中保持不變或者隨著對象的狀態而改變。 實例變數也被稱為 ...
  • 本文分享自華為雲社區《深入Python:sys模塊的功能與應用詳解》,作者: 檸檬味擁抱。 在Python的標準庫中,sys 模塊是一個常用而強大的工具,它提供了與Python解釋器交互的函數和變數。本文將介紹sys模塊的一些常用函數和方法,並通過實際的代碼實例來解析它們的用法。 1. sys.ar ...
  • 本文介紹基於Python中ArcPy模塊,實現基於柵格圖像批量裁剪柵格圖像,同時對齊各個柵格圖像的空間範圍,統一其各自行數與列數的方法~ ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...