Spring事務(wù)管理下synchronized鎖失效問題的解決方法
最近看到一個(gè)技術(shù)技術(shù)問題:synchronized鎖問題?
開啟10000個(gè)線程,每個(gè)線程給員工表的money字段【初始值是0】加1,沒有使用悲觀鎖和樂觀鎖,但是在業(yè)務(wù)層方法上加了synchronized關(guān)鍵字,問題是代碼執(zhí)行完畢后數(shù)據(jù)庫中的money 字段不是10000,而是小于10000 問題出在哪里?
Service層代碼:
SQL代碼(沒有加悲觀/樂觀鎖):
用1000個(gè)線程跑代碼:
簡單來說:多線程跑一個(gè)使用synchronized關(guān)鍵字修飾的方法,方法內(nèi)操作的是數(shù)據(jù)庫,按正常邏輯應(yīng)該最終的值是1000,但經(jīng)過多次測試,結(jié)果是低于1000。這是為什么呢?
一、我的思考
既然測試出來的結(jié)果是低于1000,那說明這段代碼不是線程安全的。不是線程安全的,那問題出現(xiàn)在哪呢?眾所周知,synchronized方法能夠保證所修飾的代碼塊、方法保證有序性、原子性、可見性。
講道理,以上的代碼跑起來,問題中Service層的increaseMoney()是有序的、原子的、可見的,所以斷定跟synchronized應(yīng)該沒關(guān)系。
既然Java層面上找不到原因,那分析一下數(shù)據(jù)庫層面的吧(因?yàn)榉椒▋?nèi)操作的是數(shù)據(jù)庫)。在increaseMoney()方法前加了@Transcational注解,說明這個(gè)方法是帶有事務(wù)的。事務(wù)能保證同組的SQL要么同時(shí)成功,要么同時(shí)失敗。講道理,如果沒有報(bào)錯(cuò)的話,應(yīng)該每個(gè)線程都對(duì)money值進(jìn)行+1。從理論上來說,結(jié)果應(yīng)該是1000的才對(duì)。
根據(jù)上面的分析,我懷疑是提問者沒測試好(hhhh,逃),于是我也跑去測試了一下,發(fā)現(xiàn)是以提問者的方式來使用是真的有問題。
首先貼一下我的測試代碼:
@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執(zhí)行的順序,如下(貼出小部分):
如下(貼出小部分):
從打印的情況我們可以得出:多線程情況下并沒有串行執(zhí)行addEmployee()方法。這就導(dǎo)致對(duì)同一個(gè)值做重復(fù)的修改,所以最終的數(shù)值比1000要少。
二、圖解出現(xiàn)的原因
發(fā)現(xiàn)并不是同步執(zhí)行的,于是我就懷疑synchronized關(guān)鍵字和Spring肯定有點(diǎn)沖突。于是根據(jù)這兩個(gè)關(guān)鍵字搜了一下,找到了問題所在。
我們知道Spring事務(wù)的底層是Spring AOP,而Spring AOP的底層是動(dòng)態(tài)代理技術(shù)。跟大家一起回顧一下動(dòng)態(tài)代理:
public static void main(String[] args) { // 目標(biāo)對(duì)象 Object target ; Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), Main.class, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 但凡帶有@Transcational注解的方法都會(huì)被攔截 // 1... 開啟事務(wù) method.invoke(target); // 2... 提交事務(wù) return null; } }); }
實(shí)際上Spring做的處理跟以上的思路是一樣的,我們可以看一下TransactionAspectSupport類中invokeWithinTransaction():
調(diào)用方法前開啟事務(wù),調(diào)用方法后提交事務(wù)
在多線程環(huán)境下,就可能會(huì)出現(xiàn):方法執(zhí)行完了(synchronized代碼塊執(zhí)行完了),事務(wù)還沒提交,別的線程可以進(jìn)入被synchronized修飾的方法,再讀取的時(shí)候,讀到的是還沒提交事務(wù)的數(shù)據(jù),這個(gè)數(shù)據(jù)不是最新的,所以就出現(xiàn)了這個(gè)問題。
三、解決問題
從上面我們可以發(fā)現(xiàn),問題所在是因?yàn)锧Transcational注解和synchronized一起使用了,加鎖的范圍沒有包括到整個(gè)事務(wù)。所以我們可以這樣做:
新建一個(gè)名叫SynchronizedService類,讓其去調(diào)用addEmployee()方法,整個(gè)代碼如下:
@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鎖的范圍包含到整個(gè)Spring事務(wù)上,這就不會(huì)出現(xiàn)線程安全的問題了。在測試的時(shí)候,我們可以發(fā)現(xiàn)1000個(gè)線程跑起來比之前要慢得多,當(dāng)然我們的數(shù)據(jù)是正確的:
拋開上面事務(wù)造成的synchronized失效問題,synchronized本身是悲觀鎖,代價(jià)偏高,像數(shù)據(jù)庫數(shù)據(jù)修改的線程安全問題,可以使用樂觀鎖,在表中添加version字段,每次修改時(shí)預(yù)期值與數(shù)據(jù)庫值比較,失敗的話一定次數(shù)自旋嘗試修改,修改成功的話version+1。
總結(jié)
到此這篇關(guān)于Spring事務(wù)管理下synchronized鎖失效問題的文章就介紹到這了,更多相關(guān)Spring事務(wù)synchronized鎖失效內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
教你如何精準(zhǔn)統(tǒng)計(jì)出你的接口"QPS"
今天小編就為大家分享一篇關(guān)于QPS的精準(zhǔn)計(jì)算方法,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2021-08-08idea中怎樣創(chuàng)建并運(yùn)行第一個(gè)java程序
這篇文章主要介紹了idea中怎樣創(chuàng)建并運(yùn)行第一個(gè)java程序問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-08-08PowerJob分布式任務(wù)調(diào)度源碼流程解讀
這篇文章主要為大家介紹了PowerJob分布式任務(wù)調(diào)度源碼流程解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2024-02-02SpringBoot搭建go-cqhttp機(jī)器人的方法實(shí)現(xiàn)
本文主要介紹了SpringBoot搭建go-cqhttp機(jī)器人的方法實(shí)現(xiàn)2021-12-12Java并發(fā)之synchronized實(shí)現(xiàn)原理深入理解
這篇文章主要介紹了Java中synchronized實(shí)現(xiàn)原理詳解,涉及synchronized實(shí)現(xiàn)同步的基礎(chǔ),Java對(duì)象頭,Monitor,Mark Word,鎖優(yōu)化,自旋鎖等相關(guān)內(nèi)容,具有一定借鑒價(jià)值,需要的朋友可以參考下2021-08-08基于@RestControllerAdvice與@ControllerAdvice的區(qū)別說明
這篇文章主要介紹了@RestControllerAdvice與@ControllerAdvice的區(qū)別說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-09-09Java實(shí)現(xiàn)Html轉(zhuǎn)Pdf的方法
這篇文章主要介紹了Java實(shí)現(xiàn)Html轉(zhuǎn)Pdf的方法,實(shí)例分析了java基于ITextRenderer類操作頁面及系統(tǒng)自帶字體生成pdf文件的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-07-07Spring boot打包jar分離lib和resources方法實(shí)例
這篇文章主要介紹了Spring boot打包jar分離lib和resources方法實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05