摘要:不是線程安全的,那問題出現(xiàn)在哪呢眾所周知,方法能夠保證所修飾的代碼塊方法保證有序性原子性可見性。講道理,如果沒有報錯的話,應(yīng)該每個線程都對值進行。
前言
只有光頭才能變強。
文本已收錄至我的GitHub倉庫,歡迎Star:https://github.com/ZhongFuCheng3y/3y
大年初二,朋友問了我一個技術(shù)的問題(朋友實在是好學(xué),佩服!)
該問題來源知乎(synchronized鎖問題):
https://www.zhihu.com/question/277812143
開啟10000個線程,每個線程給員工表的money字段【初始值是0】加1,沒有使用悲觀鎖和樂觀鎖,但是在業(yè)務(wù)層方法上加了synchronized關(guān)鍵字,問題是代碼執(zhí)行完畢后數(shù)據(jù)庫中的money 字段不是10000,而是小于10000 問題出在哪里?
Service層代碼:
SQL代碼(沒有加悲觀/樂觀鎖):
用1000個線程跑代碼:
簡單來說:多線程跑一個使用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)系。
(參考我之前寫過的synchronize鎖筆記:Java鎖機制了解一下)
既然Java層面上找不到原因,那分析一下數(shù)據(jù)庫層面的吧(因為方法內(nèi)操作的是數(shù)據(jù)庫)。在increaseMoney()方法前加了@Transcational注解,說明這個方法是帶有事務(wù)的。事務(wù)能保證同組的SQL要么同時成功,要么同時失敗。講道理,如果沒有報錯的話,應(yīng)該每個線程都對money值進行+1。從理論上來說,結(jié)果應(yīng)該是1000的才對。
(參考我之前寫過的Spring事務(wù):一文帶你看懂Spring事務(wù)!)
根據(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)致對同一個值做重復(fù)的修改,所以最終的數(shù)值比1000要少。
二、圖解出現(xiàn)的原因發(fā)現(xiàn)并不是同步執(zhí)行的,于是我就懷疑synchronized關(guān)鍵字和Spring肯定有點沖突。于是根據(jù)這兩個關(guān)鍵字搜了一下,找到了問題所在。
我們知道Spring事務(wù)的底層是Spring AOP,而Spring AOP的底層是動態(tài)代理技術(shù)。跟大家一起回顧一下動態(tài)代理:
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... 開啟事務(wù) method.invoke(target); // 2... 提交事務(wù) return null; } }); }
(詳細請參考我之前寫過的動態(tài)代理:給女朋友講解什么是代理模式)
實際上Spring做的處理跟以上的思路是一樣的,我們可以看一下TransactionAspectSupport類中invokeWithinTransaction():
調(diào)用方法前開啟事務(wù),調(diào)用方法后提交事務(wù)
在多線程環(huán)境下,就可能會出現(xiàn):方法執(zhí)行完了(synchronized代碼塊執(zhí)行完了),事務(wù)還沒提交,別的線程可以進入被synchronized修飾的方法,再讀取的時候,讀到的是還沒提交事務(wù)的數(shù)據(jù),這個數(shù)據(jù)不是最新的,所以就出現(xiàn)了這個問題。
三、解決問題從上面我們可以發(fā)現(xiàn),問題所在是因為@Transcational注解和synchronized一起使用了,加鎖的范圍沒有包括到整個事務(wù)。所以我們可以這樣做:
新建一個名叫SynchronizedService類,讓其去調(diào)用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事務(wù)上,這就不會出現(xiàn)線程安全的問題了。在測試的時候,我們可以發(fā)現(xiàn)1000個線程跑起來比之前要慢得多,當(dāng)然我們的數(shù)據(jù)是正確的:
最后可以發(fā)現(xiàn)的是,雖然說Spring事務(wù)用起來我們是非常方便的,但如果不了解一些Spring事務(wù)的細節(jié),很多時候出現(xiàn)Bug了就百思不得其解。還是得繼續(xù)加油努力呀~~~
樂于輸出干貨的Java技術(shù)公眾號:Java3y。公眾號內(nèi)有200多篇原創(chuàng)技術(shù)文章、海量視頻資源、精美腦圖,不妨來關(guān)注一下!
覺得我的文章寫得不錯,不妨點一下贊!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/73312.html
摘要:使用靜態(tài)類體現(xiàn)的是基于對象,而使用單例設(shè)計模式體現(xiàn)的是面向?qū)ο?。二編寫單例模式的代碼編寫單例模式的代碼其實很簡單,就分了三步將構(gòu)造函數(shù)私有化在類的內(nèi)部創(chuàng)建實例提供獲取唯一實例的方法餓漢式根據(jù)上面的步驟,我們就可以輕松完成創(chuàng)建單例對象了。 前言 只有光頭才能變強 回顧前面: 給女朋友講解什么是代理模式 包裝模式就是這么簡單啦 本來打算沒那么快更新的,這陣子在刷Spring的書籍。在看...
摘要:又是金三銀四的時候,我希望這份面試題能夠祝你一臂之力自我和項目相關(guān)自我介紹你覺得自己的優(yōu)點是你覺得自己有啥缺點你有哪些你為什么要離開上家公司你上家公司在,我們公司在,離這么遠為什么要選擇我們這里上家公司的同事和領(lǐng)導(dǎo)是怎么評價你的介紹下你的上 又是金三銀四的時候,我希望這份面試題能夠祝你一臂之力! 自我和項目相關(guān) 1、自我介紹 2、你覺得自己的優(yōu)點是?你覺得自己有啥缺點? 3、你有哪些 ...
摘要:結(jié)構(gòu)型模式適配器模式橋接模式裝飾模式組合模式外觀模式享元模式代理模式。行為型模式模版方法模式命令模式迭代器模式觀察者模式中介者模式備忘錄模式解釋器模式模式狀態(tài)模式策略模式職責(zé)鏈模式責(zé)任鏈模式訪問者模式。 主要版本 更新時間 備注 v1.0 2015-08-01 首次發(fā)布 v1.1 2018-03-12 增加新技術(shù)知識、完善知識體系 v2.0 2019-02-19 結(jié)構(gòu)...
摘要:本文旨在對鎖相關(guān)源碼本文中的源碼來自使用場景進行舉例,為讀者介紹主流鎖的知識點,以及不同的鎖的適用場景。中,關(guān)鍵字和的實現(xiàn)類都是悲觀鎖。自適應(yīng)意味著自旋的時間次數(shù)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來決定。 前言 Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當(dāng)?shù)膱鼍跋履軌蛘宫F(xiàn)出非常高的效率。本文旨在對鎖相關(guān)源碼(本文中的源碼來自JDK 8)、使用場景...
摘要:用戶態(tài)不能干擾內(nèi)核態(tài)所以指令就有兩種特權(quán)指令和非特權(quán)指令不同的狀態(tài)對應(yīng)不同的指令。非特權(quán)指令所有程序均可直接使用。用戶態(tài)常態(tài)目態(tài)執(zhí)行非特權(quán)指令。 這是我今年從三月份開始,主要的大廠面試經(jīng)過,有些企業(yè)面試的還沒來得及整理,可能有些沒有帶答案就發(fā)出來了,還請各位先思考如果是你怎么回答面試官?這篇文章會持續(xù)更新,請各位持續(xù)關(guān)注,希望對你有所幫助! 面試清單 平安產(chǎn)險 飛豬 上汽大通 浩鯨科...
閱讀 2895·2023-04-26 02:49
閱讀 3461·2021-11-25 09:43
閱讀 3437·2021-10-09 09:43
閱讀 3020·2021-09-28 09:44
閱讀 2461·2021-09-22 15:29
閱讀 4538·2021-09-14 18:02
閱讀 2794·2021-09-03 10:48
閱讀 3438·2019-08-30 12:47