前言
只有光頭才能變強。
文字已收錄至我的GitHub倉庫,歡迎Star:github.com/ZhongFuChen…
大年初二,朋友問了我一個技術的問題(朋友實在是好學,佩服!)
該問題來源知乎(synchronized鎖問題):
開啟10000個執行緒,每個執行緒給員工表的money欄位【初始值是0】加1,沒有使用悲觀鎖和樂觀鎖,但是在業務層方法上加了synchronized關鍵字,問題是程式碼執行完畢後資料庫中的money 欄位不是10000,而是小於10000 問題出在哪裡?
Service層程式碼:
SQL程式碼(沒有加悲觀/樂觀鎖):
用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執行的順序,如下(貼出小部分):
從列印的情況我們可以得出:多執行緒情況下並沒有序列執行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()
:
呼叫方法前開啟事務,呼叫方法後提交事務
在多執行緒環境下,就可能會出現:方法執行完了(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多篇原創技術文章、海量視訊資源、精美腦圖,不妨來關注一下!
覺得我的文章寫得不錯,不妨點一下贊!