Synchronized鎖在Spring事務管理下,為啥還線程不安全?

来源:https://www.cnblogs.com/Java3y/archive/2019/02/17/10392645.html
-Advertisement-
Play Games

前言 只有光頭才能變強。 文本已收錄至我的GitHub倉庫,歡迎Star: "https://github.com/ZhongFuCheng3y/3y" 大年初二,朋友問了我一個技術的問題(朋友實在是好學,佩服!) 該問題來源知乎(synchronized鎖問題): "https://www.zhi ...


前言

只有光頭才能變強。

文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y

大年初二,朋友問了我一個技術的問題(朋友實在是好學,佩服!)

該問題來源知乎(synchronized鎖問題):

開啟10000個線程,每個線程給員工表的money欄位【初始值是0】加1,沒有使用悲觀鎖和樂觀鎖,但是在業務層方法上加了synchronized關鍵字,問題是代碼執行完畢後資料庫中的money 欄位不是10000,而是小於10000 問題出在哪裡?

Service層代碼:

代碼

SQL代碼(沒有加悲觀/樂觀鎖):

SQL代碼(沒有加悲觀/樂觀鎖)

用1000個線程跑代碼:

用1000個線程跑代碼:

簡單來說:多線程跑一個使用synchronized關鍵字修飾的方法,方法內操作的是資料庫,按正常邏輯應該最終的值是1000,但經過多次測試,結果是低於1000。這是為什麼呢?

一、我的思考

既然測試出來的結果是低於1000,那說明這段代碼不是線程安全的。不是線程安全的,那問題出現在哪呢?眾所周知,synchronized方法能夠保證所修飾的代碼塊、方法保證有序性、原子性、可見性

講道理,以上的代碼跑起來,問題中Service層的increaseMoney()有序的、原子的、可見的,所以斷定跟synchronized應該沒關係。

(參考我之前寫過的synchronize鎖筆記:Java鎖機制瞭解一下)

既然Java層面上找不到原因,那分析一下資料庫層面的吧(因為方法內操作的是資料庫)。在increaseMoney()方法前加了@Transcational註解,說明這個方法是帶有事務的。事務能保證同組的SQL要麼同時成功,要麼同時失敗。講道理,如果沒有報錯的話,應該每個線程都對money值進行+1。從理論上來說,結果應該是1000的才對。

(參考我之前寫過的Spring事務:一文帶你看懂Spring事務!)

根據上面的分析,我懷疑是提問者沒測試好(hhhh,逃),於是我也跑去測試了一下,發現是以提問者的方式來使用是真的有問題

首先貼一下我的測試代碼:


@RestController
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @RequestMapping("/add")
    public void addEmployee() {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> employeeService.addEmployee()).start();
        }
    }


}

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;


    @Transactional
    public synchronized void addEmployee() {

        // 查出ID為8的記錄,然後每次將年齡增加一
        Employee employee = employeeRepository.getOne(8);
        System.out.println(employee);
        Integer age = employee.getAge();
        employee.setAge(age + 1);

        employeeRepository.save(employee);
    }

}

簡單地列印了每次拿到的employee值,並且拿到了SQL執行的順序,如下(貼出小部分):

SQL執行的順序

從列印的情況我們可以得出:多線程情況下並沒有串列執行addEmployee()方法。這就導致對同一個值做重覆的修改,所以最終的數值比1000要少。

二、圖解出現的原因

發現並不是同步執行的,於是我就懷疑synchronized關鍵字和Spring肯定有點衝突。於是根據這兩個關鍵字搜了一下,找到了問題所在。

我們知道Spring事務的底層是Spring AOP,而Spring AOP的底層是動態代理技術。跟大家一起回顧一下動態代理:


    public static void main(String[] args) {

        // 目標對象
        Object target ;

        Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                // 但凡帶有@Transcational註解的方法都會被攔截

                // 1... 開啟事務

                method.invoke(target);

                // 2... 提交事務

                return null;
            }
            
        });
    }

(詳細請參考我之前寫過的動態代理:給女朋友講解什麼是代理模式)

實際上Spring做的處理跟以上的思路是一樣的,我們可以看一下TransactionAspectSupport類中invokeWithinTransaction()

Spring事務管理是如何實現的

調用方法開啟事務,調用方法提交事務

Spring事務和synchronized鎖互斥問題

在多線程環境下,就可能會出現:方法執行完了(synchronized代碼塊執行完了),事務還沒提交,別的線程可以進入被synchronized修飾的方法,再讀取的時候,讀到的是還沒提交事務的數據,這個數據不是最新的,所以就出現了這個問題。

事務未提交,別的線程讀取到舊數據

三、解決問題

從上面我們可以發現,問題所在是因為@Transcational註解和synchronized一起使用了,加鎖的範圍沒有包括到整個事務。所以我們可以這樣做:

新建一個名叫SynchronizedService類,讓其去調用addEmployee()方法,整個代碼如下:


@RestController
public class EmployeeController {

    @Autowired
    private SynchronizedService synchronizedService ;

    @RequestMapping("/add")
    public void addEmployee() {
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> synchronizedService.synchronizedAddEmployee()).start();
        }
    }
}

// 新建的Service類
@Service
public class SynchronizedService {

    @Autowired
    private EmployeeService employeeService ;
    
    // 同步
    public synchronized void synchronizedAddEmployee() {
        employeeService.addEmployee();

    }
}

@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    
    @Transactional
    public void addEmployee() {

        // 查出ID為8的記錄,然後每次將年齡增加一
        Employee employee = employeeRepository.getOne(8);
        System.out.println(Thread.currentThread().getName() + employee);
        Integer age = employee.getAge();
        employee.setAge(age + 1);

        employeeRepository.save(employee);

    }
}

我們將synchronized鎖的範圍包含到整個Spring事務上,這就不會出現線程安全的問題了。在測試的時候,我們可以發現1000個線程跑起來比之前要慢得多,當然我們的數據是正確的:

正確的數據

最後

可以發現的是,雖然說Spring事務用起來我們是非常方便的,但如果不瞭解一些Spring事務的細節,很多時候出現Bug了就百思不得其解。還是得繼續加油努力呀~~~

樂於輸出乾貨的Java技術公眾號:Java3y。公眾號內有200多篇原創技術文章、海量視頻資源、精美腦圖,不妨來關註一下!

帥的人都關註了

覺得我的文章寫得不錯,不妨點一下


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

-Advertisement-
Play Games
更多相關文章
  • import randomimport sysimport zipfileimport timefrom threading import Threadfrom multiprocessing import Processclass MyIterator: # letters = '01234567... ...
  • Go語言是Google公司在2009年開源的一門高級編程語言,它為解決大型系統開發過程中的實際問題而設計,支持併發、規範統一、簡單優雅,被很多Go語言傳道者譽為“互聯網時代的C語言”。而C++語言誕生於1979年,可以將C++語言視為一個語言聯邦,主要包含C語言(面向過程)、面向對象、STL容器和算 ...
  • 最近複習JVM的知識,對於靜態分派和動態分派的理解有點混亂,於是自己嘗試寫寫代碼,在分析中鞏固知識。 有如下一段代碼,請問每一段分別輸出什麼? 下麵我簡單地介紹一下從代碼編譯到方法調用的整個過程。 · 編譯 先看看第1段輸出,child.foo()是調用父類還是子類的靜態方法呢? 在編譯階段,發生了 ...
  • 看門見山 1.java中replace API: replace(char oldChar, char newChar):寓意為:返回一個新的字元串,它是通過用 newChar 替換此字元串中出現的所有 oldChar 得到的。 replace(CharSequence target, CharSe ...
  • 比如我們有個列表: 如果我們需要將列表裡的元素轉換為數字呢?最常用的大家可能會想到使用列表推導式: 輸出:[1, 2, 3, 4] 還有一種技巧,更方便: 輸出:[1, 2, 3, 4] ...
  • 思路 首先想到費用流。 對於每個點拆點。然後考慮我們怎樣才能保證每個點只被用一次。 如果$i$與$j$滿足條件。那麼就從$i$向$j$連一條邊並且從$j$向$i$連一條 ...
  • 原創文章,轉載請標註出處:[《Java基礎系列 Comparable和Comparator》](https://www.jianshu.com/p/f9870fd05958 一、概述 & 160;& 160;& 160;& 160;& 160;& 160;& 160;& 160;Java中的排序是由 ...
  • 抽時間複習 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...