quarkus依賴註入之九:bean讀寫鎖

来源:https://www.cnblogs.com/bolingcavalry/archive/2023/08/08/17608084.html
-Advertisement-
Play Games

### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本篇是《quarkus依賴註入》的第九篇 ...


歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 本篇是《quarkus依賴註入》的第九篇,目標是在輕鬆的氣氛中學習一個小技能:bean鎖
  • quarkus的bean鎖本身很簡單:用兩個註解修飾bean和方法即可,但涉及到多線程同步問題,欣宸願意花更多篇幅與各位Java程式員一起暢談多線程,聊個痛快,本篇由以下內容組成
  1. 關於多線程同步問題
  2. 代碼復現多線程同步問題
  3. quarkus的bean讀寫鎖

關於讀寫鎖

  • java的併發包中有讀寫鎖ReadWriteLock:在多線程場景中,如果某個對象處於改變狀態,可以用寫鎖加鎖,這樣所有做讀操作對象的線程,在獲取讀鎖時就會block住,直到寫鎖釋放
  • 為了演示bean鎖的效果,咱們先來看一個經典的多線程同步問題,如下圖,餘額100,充值10塊,扣費5塊,正常情況下最終餘額應該是105,但如果充值和扣費是在兩個線程同時進行,而且各算各的,再分別用自己的計算結果去覆蓋餘額,最終會導致計算不准確
流程圖 (2)

代碼復現多線程同步問題

  • 咱們用代碼來複現上圖中的問題,AccountBalanceService是個賬號服務類,其成員變數accountBalance表示餘額,另外有三個方法,功能分別是:
  1. get:返回餘額,相當於查詢餘額服務
  2. deposit:充值,入參是充值金額,方法內將餘額放入臨時變數,然後等待100毫秒模擬耗時操作,再將臨時變數與入參的和寫入成員變數accountBalance
  3. deduct:扣費,入參是扣費金額,方法內將餘額放入臨時變數,然後等待100毫秒模擬耗時操作,再將臨時變數與入參的差寫入成員變數accountBalance
  • AccountBalanceService.java源碼如下,deposit和deduct這兩個方法各算各的,絲毫沒有考慮當時其他線程對accountBalance的影響
package com.bolingcavalry.service.impl;

import io.quarkus.logging.Log;
import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class AccountBalanceService {

    // 賬戶餘額,假設初始值為100
    int accountBalance = 100;

    /**
     * 查詢餘額
     * @return
     */
    public int get() {
        // 模擬耗時的操作
        try {
            Thread.sleep(80);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return accountBalance;
    }

    /**
     * 模擬了一次充值操作,
     * 將賬號餘額讀取到本地變數,
     * 經過一秒鐘的計算後,將計算結果寫入賬號餘額,
     * 這一秒內,如果賬號餘額發生了變化,就會被此方法的本地變數覆蓋,
     * 因此,多線程的時候,如果其他線程修改了餘額,那麼這裡就會覆蓋掉,導致多線程同步問題,
     * AccountBalanceService類使用了Lock註解後,執行此方法時,其他線程執行AccountBalanceService的方法時就會block住,避免了多線程同步問題
     * @param value
     * @throws InterruptedException
     */
    public void deposit(int value) {
        // 先將accountBalance的值存入tempValue變數
        int tempValue  = accountBalance;
        Log.infov("start deposit, balance [{0}], deposit value [{1}]", tempValue, value);

        // 模擬耗時的操作
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        tempValue += value;

        // 用tempValue的值覆蓋accountBalance,
        // 這個tempValue的值是基於100毫秒前的accountBalance計算出來的,
        // 如果這100毫秒期間其他線程修改了accountBalance,就會導致accountBalance不准確的問題
        // 例如最初有100塊,這裡存了10塊,所以餘額變成了110,
        // 但是這期間如果另一線程取了5塊,那餘額應該是100-5+10=105,但是這裡並沒有靠攏100-5,而是很暴力的將110寫入到accountBalance
        accountBalance = tempValue;

        Log.infov("end deposit, balance [{0}]", tempValue);
    }

    /**
     * 模擬了一次扣費操作,
     * 將賬號餘額讀取到本地變數,
     * 經過一秒鐘的計算後,將計算結果寫入賬號餘額,
     * 這一秒內,如果賬號餘額發生了變化,就會被此方法的本地變數覆蓋,
     * 因此,多線程的時候,如果其他線程修改了餘額,那麼這裡就會覆蓋掉,導致多線程同步問題,
     * AccountBalanceService類使用了Lock註解後,執行此方法時,其他線程執行AccountBalanceService的方法時就會block住,避免了多線程同步問題
     * @param value
     * @throws InterruptedException
     */
    public void deduct(int value) {
        // 先將accountBalance的值存入tempValue變數
        int tempValue  = accountBalance;
        Log.infov("start deduct, balance [{0}], deposit value [{1}]", tempValue, value);

        // 模擬耗時的操作
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        tempValue -= value;

        // 用tempValue的值覆蓋accountBalance,
        // 這個tempValue的值是基於100毫秒前的accountBalance計算出來的,
        // 如果這100毫秒期間其他線程修改了accountBalance,就會導致accountBalance不准確的問題
        // 例如最初有100塊,這裡存了10塊,所以餘額變成了110,
        // 但是這期間如果另一線程取了5塊,那餘額應該是100-5+10=105,但是這裡並沒有靠攏100-5,而是很暴力的將110寫入到accountBalance
        accountBalance = tempValue;

        Log.infov("end deduct, balance [{0}]", tempValue);
    }
}
  • 接下來是單元測試類LockTest.java,有幾處需要註意的地方稍後會說明
package com.bolingcavalry;

import com.bolingcavalry.service.impl.AccountBalanceService;
import io.quarkus.logging.Log;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import javax.inject.Inject;
import java.util.concurrent.CountDownLatch;

@QuarkusTest
public class LockTest {

    @Inject
    AccountBalanceService account;

    @Test
    public void test() throws InterruptedException {
        CountDownLatch latch = new CountDownLatch(3);
        int initValue = account.get();

        final int COUNT = 10;

        // 這是個只負責讀取的線程,迴圈讀10次,每讀一次就等待50毫秒
        new Thread(() -> {

            for (int i=0;i<COUNT;i++) {
                // 讀取賬號餘額
                Log.infov("current balance {0}", account.get());
            }

            latch.countDown();
        }).start();

        // 這是個充值的線程,迴圈充10次,每次存2元
        new Thread(() -> {
            for (int i=0;i<COUNT;i++) {
                account.deposit(2);
            }
            latch.countDown();
        }).start();

        // 這是個扣費的線程,迴圈扣10次,每取1元
        new Thread(() -> {
            for (int i=0;i<COUNT;i++) {
                account.deduct(1);
            }
            latch.countDown();
        }).start();

        latch.await();

        int finalValue = account.get();
        Log.infov("finally, current balance {0}", finalValue);
        Assertions.assertEquals(initValue + COUNT, finalValue);
    }
}
  • 上述代碼中,有以下幾點需要註意
  1. 在主線程中新增了三個子線程,分別執行查詢、充值、扣費的操作,可見deposit和deduct方法是並行執行的
  2. 初始餘額100,充值一共20元,扣費一共10元,因此最終正確結果應該是110元
  3. 為了確保三個子線程全部執行完畢後主線程才退出,這裡用了CountDownLatch,在執行latch.await()的時候主線程就開始等待了,等到三個子線程把各自的latch.await()都執行後,主線程才會繼續執行
  4. 最終會檢查餘額是否等於110,如果不是則單元測試不通過
  • 執行單元測試,結果如下圖,果然失敗了
image-20220417105801982
  • 來分析測試過程中的日誌,有助於我們理解問題的原因,如下圖,充值和扣費同時開始,充值先完成,此時餘額是102,但是扣費無視102,依舊使用100作為餘額去扣費,然後將扣費結果99寫入餘額,導致餘額與正確的邏輯產生差距

16

  • 反覆運行上述單元測試,可以發現每次得到的結果都不一樣,這算是典型的多線程同步問題了吧...
  • 看到這裡,經驗豐富的您應該想到了多種解決方式,例如下麵這五種都可以:
  1. 用傳統的synchronized關鍵字修飾三個方法
  2. java包的讀寫鎖
  3. deposit和deduct方法內部,不要使用臨時變數tempValue,將餘額的類型從int改成AtomicInteger,再使用addAndGet方法計算並設置
  4. 用MySQL的樂觀鎖
  5. 用Redis的分散式鎖
  • 沒錯,上述方法都能解決問題,現在除了這些,quarku還從bean的維度為我們提供了一種新的方法:bean讀寫鎖,接下來細看這個bean讀寫鎖

Container-managed Concurrency:quarkus基於bean的讀寫鎖方案

  • quarkus為bean提供了讀寫鎖方案:Lock註解,藉助它,可以為bean的所有方法添加同一把寫鎖,再手動將讀鎖添加到指定的讀方法,這樣在多線程操作的場景下,也能保證數據的正確性
  • 來看看Lock註解源碼,很簡單的幾個屬性,要重點註意的是:預設屬性為Type.WRITE,也就是寫鎖,被Lock修飾後,鎖類型有三種選擇:讀鎖,寫鎖,無鎖
@InterceptorBinding
@Inherited
@Target(value = { TYPE, METHOD })
@Retention(value = RUNTIME)
public @interface Lock {

    /**
     * 
     * @return the type of the lock
     */
    @Nonbinding
    Type value() default Type.WRITE;

    /**
     * If it's not possible to acquire the lock in the given time a {@link LockException} is thrown.
     * 
     * @see java.util.concurrent.locks.Lock#tryLock(long, TimeUnit)
     * @return the wait time
     */
    @Nonbinding
    long time() default -1l;

    /**
     * 
     * @return the wait time unit
     */
    @Nonbinding
    TimeUnit unit() default TimeUnit.MILLISECONDS;

    public enum Type {
        /**
         * Acquires the read lock before the business method is invoked.
         */
        READ,
        /**
         * Acquires the write (exclusive) lock before the business method is invoked.
         */
        WRITE,
        /**
         * Acquires no lock.
         * <p>
         * This could be useful if you need to override the behavior defined by a class-level interceptor binding.
         */
        NONE
    }

}
  • 接下來看看如何用bean鎖解AccountBalanceService的多線程同步問題

  • 為bean設置讀寫鎖很簡單,如下圖紅框1,給類添加Lock註解後,AccountBalanceService的每個方法都預設添加了寫鎖,如果想修改某個方法的鎖類型,可以像紅框2那樣指定,Lock.Type.READ表示將get方法改為讀鎖,如果不想給方法上任何鎖,就使用Lock.Type.NONE

image-20220417113205821
  • 這裡預測一下修改後的效果
  1. 在deposit和deduct都沒有被調用時,get方法可以被調用,而且可以多線程同時調用,因為每個線程都能順利拿到讀鎖
  2. 一旦deposit或者deduct被調用,其他線程在調用deposit、deduct、get方法時都被阻塞了,因為此刻不論讀鎖還是寫鎖都拿不到,必須等deposit執行完畢,它們才重新去搶鎖
  3. 有了上述邏輯,再也不會出現deposit和deduct同時修改餘額的情況了,預測單元測試應該能通過
  4. 這種讀寫鎖的方法雖然可以確保邏輯正確,但是代價不小(一個線程執行,其他線程等待),所以在併發性能要求較高的場景下要慎用,可以考慮樂觀鎖、AtomicInteger這些方式來降低等待代價
  • 再次運行單元測試,如下圖,測試通過
image-20220417120035378
  • 再來看看測試過程中的日誌,如下圖,之前的幾個方法同時執行的情況已經消失了,每個方法在執行的時候,其他線程都在等待

image-20220417120428928

  • 至此,bean鎖知識點學習完畢,希望本篇能給您一些參考,為您的併發編程中添加新的方案

源碼下載

名稱 鏈接 備註
項目主頁 https://github.com/zq2599/blog_demos 該項目在GitHub上的主頁
git倉庫地址(https) https://github.com/zq2599/blog_demos.git 該項目源碼的倉庫地址,https協議
git倉庫地址(ssh) [email protected]:zq2599/blog_demos.git 該項目源碼的倉庫地址,ssh協議
  • 這個git項目中有多個文件夾,本次實戰的源碼在quarkus-tutorials文件夾下,如下圖紅框
    image-20220312091203116
  • quarkus-tutorials是個父工程,裡面有多個module,本篇實戰的module是basic-di,如下圖紅框
    image-20220312091404031

歡迎關註博客園:程式員欣宸

學習路上,你不孤單,欣宸原創一路相伴...


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

-Advertisement-
Play Games
更多相關文章
  • 基於本次618大促JDV平臺支持大促過程中的表現,共從大促總結、能力沉澱、待提升項3個方向也進行了相應總結和反思。 ...
  • > 問:有一個postgres數據表,表中有update_time, create_time關於時間戳的欄位,選擇某個時間段,計算出update_time減去create_time的值做為耗時時間的欄位duration_time,統計出在這段時間內耗時時間的平均值,中位數值,最大值,和最小值,請問如 ...
  • 我們非常高興的發佈為了一年一度的SIGGRAPH 2023發佈關於為PostGIS支持USD格式的新拓展。 新添加了3個函數 ST_AsUSDA(geom geometry, usd_root_name text, usd_geom_name text, width float) ST_AsUSDC ...
  • 原文地址:https://zhanglei.blog.csdn.net/article/details/121673288 [toc] ## 一、前言 應用中某些模塊需要組件化,組件化後的工程最後會做二進位化處理,打包成`.framework`文件。 今天簡單聊一下在主工程或其他組件中是如何訪問自製 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. 場景 前端構建完上線,用戶還停留還在老頁面,用戶不知道網頁重新部署了,跳轉頁面的時候有時候js連接hash變了導致報錯跳不過去,並且用戶體驗不到新功能。 2. 解決方案 每次打包寫入一個json文件,或者對比生成的script的sr ...
  • 實現一個項目匹配多個端,使用vue.config自帶的page 實現多個頁面切換。官網介紹:https://cli.vuejs.org/zh/config/#pages 在創建的vue項目中找到 vue.config.js中 添加page 沒有就在根目錄下創建vue.config.js const ...
  • # 1 HTML5新特性 ## 1.1 概述 HTML5 的新增特性主要是針對於以前的不足,增加了一些新的標簽、新的表單和新的表單屬性等。 這些新特性都有相容性問題,基本是 **IE9+ 以上版本的瀏覽器**才支持,如果不考慮相容性問題,可以大量使用這些新特性。 ## 1.2 語義化標簽 (★★) ...
  • 系統要求 C/S架構的單體桌面應用,可以滿足客戶個性化需求,易於升級和維護。相比於一代Winform,界面要求美觀,控制項豐富可定製。 解決方案 依托.Net6開發平臺,採用模塊化思想設計(即分而治之的策略),每個模塊採用DDD分層設計。前端選用WPF + Prism框架,後端選用ABP + EF框架 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...