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


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

前言

轉載 https://juejin.im/post/5c695b44e51d45164c759c36

該問題來源知乎(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了就百思不得其解。還是得繼續加油努力呀~~~


作者:Java3y
鏈接:https://juejin.im/post/5c695b44e51d45164c759c36
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM