javascript
同一事务多次加for_Synchronized锁在Spring事务管理下,为啥还线程不安全?
前言
只有光頭才能變強(qiáng)。文本已收錄至我的GitHub倉(cāng)庫(kù),歡迎Star:https://github.com/ZhongFuCheng3y/3y
大年初二,朋友問了我一個(gè)技術(shù)的問題(朋友實(shí)在是好學(xué),佩服!)
該問題來源知乎(synchronized鎖問題):
- https://www.zhihu.com/question/277812143
Service層代碼:
SQL代碼(沒有加悲觀/樂觀鎖):
用1000個(gè)線程跑代碼:
簡(jiǎn)單來說:多線程跑一個(gè)使用synchronized關(guān)鍵字修飾的方法,方法內(nèi)操作的是數(shù)據(jù)庫(kù),按正常邏輯應(yīng)該最終的值是1000,但經(jīng)過多次測(cè)試,結(jié)果是低于1000。這是為什么呢?
一、我的思考
既然測(cè)試出來的結(jié)果是低于1000,那說明這段代碼不是線程安全的。不是線程安全的,那問題出現(xiàn)在哪呢?眾所周知,synchronized方法能夠保證所修飾的代碼塊、方法保證有序性、原子性、可見性。
講道理,以上的代碼跑起來,問題中Service層的increaseMoney()是有序的、原子的、可見的,所以斷定跟synchronized應(yīng)該沒關(guān)系。
(參考我之前寫過的synchronize鎖筆記:Java鎖機(jī)制了解一下)
既然Java層面上找不到原因,那分析一下數(shù)據(jù)庫(kù)層面的吧(因?yàn)榉椒▋?nèi)操作的是數(shù)據(jù)庫(kù))。在increaseMoney()方法前加了@Transcational注解,說明這個(gè)方法是帶有事務(wù)的。事務(wù)能保證同組的SQL要么同時(shí)成功,要么同時(shí)失敗。講道理,如果沒有報(bào)錯(cuò)的話,應(yīng)該每個(gè)線程都對(duì)money值進(jìn)行+1。從理論上來說,結(jié)果應(yīng)該是1000的才對(duì)。
(參考我之前寫過的Spring事務(wù):一文帶你看懂Spring事務(wù)!)
根據(jù)上面的分析,我懷疑是提問者沒測(cè)試好(hhhh,逃),于是我也跑去測(cè)試了一下,發(fā)現(xiàn)是以提問者的方式來使用是真的有問題。
首先貼一下我的測(cè)試代碼:
@RestController public class EmployeeController {@Autowiredprivate EmployeeService employeeService;@RequestMapping("/add")public void addEmployee() {for (int i = 0; i < 1000; i++) {new Thread(() -> employeeService.addEmployee()).start();}}}@Service public class EmployeeService {@Autowiredprivate EmployeeRepository employeeRepository;@Transactionalpublic 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);}}簡(jiǎn)單地打印了每次拿到的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() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 但凡帶有@Transcational注解的方法都會(huì)被攔截// 1... 開啟事務(wù)method.invoke(target);// 2... 提交事務(wù)return null;}});}(詳細(xì)請(qǐng)參考我之前寫過的動(dòng)態(tài)代理:給女朋友講解什么是代理模式)
實(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)?#64;Transcational注解和synchronized一起使用了,加鎖的范圍沒有包括到整個(gè)事務(wù)。所以我們可以這樣做:
新建一個(gè)名叫SynchronizedService類,讓其去調(diào)用addEmployee()方法,整個(gè)代碼如下:
@RestController public class EmployeeController {@Autowiredprivate SynchronizedService synchronizedService ;@RequestMapping("/add")public void addEmployee() {for (int i = 0; i < 1000; i++) {new Thread(() -> synchronizedService.synchronizedAddEmployee()).start();}} }// 新建的Service類 @Service public class SynchronizedService {@Autowiredprivate EmployeeService employeeService ;// 同步public synchronized void synchronizedAddEmployee() {employeeService.addEmployee();} }@Service public class EmployeeService {@Autowiredprivate EmployeeRepository employeeRepository;@Transactionalpublic 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)線程安全的問題了。在測(cè)試的時(shí)候,我們可以發(fā)現(xiàn)1000個(gè)線程跑起來比之前要慢得多,當(dāng)然我們的數(shù)據(jù)是正確的:
最后
可以發(fā)現(xiàn)的是,雖然說Spring事務(wù)用起來我們是非常方便的,但如果不了解一些Spring事務(wù)的細(xì)節(jié),很多時(shí)候出現(xiàn)Bug了就百思不得其解。還是得繼續(xù)加油努力呀~~~
樂于輸出干貨的Java技術(shù)公眾號(hào):Java3y。公眾號(hào)內(nèi)有200多篇原創(chuàng)技術(shù)文章、海量視頻資源、精美腦圖,不妨來關(guān)注一下!覺得我的文章寫得不錯(cuò),不妨點(diǎn)一下贊!
總結(jié)
以上是生活随笔為你收集整理的同一事务多次加for_Synchronized锁在Spring事务管理下,为啥还线程不安全?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 万全服务器t350装系统_计算机中那些事
- 下一篇: python中utf8占几个字节_为什么