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

開啓10000個線程,每一個線程給員工表的money字段【初始值是0】加1,沒有使用悲觀鎖和樂觀鎖,可是在業務層方法上加了synchronized關鍵字,問題是代碼執行完畢後數據庫中的money 字段不是10000,而是小於10000 問題出在哪裏?數據庫

Service層代碼:安全

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

 

SQL代碼(沒有加悲觀/樂觀鎖):多線程

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

 

用1000個線程跑代碼:app

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

 

簡單來講:多線程跑一個使用synchronized關鍵字修飾的方法,方法內操做的是數據庫,按正常邏輯應該最終的值是1000,但通過屢次測試,結果是低於1000。這是爲何呢?分佈式

1、個人思考ide

既然測試出來的結果是低於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執行的順序,以下(貼出小部分):

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

 

從打印的狀況咱們能夠得出:多線程狀況下並無串行執行addEmployee()方法。這就致使對同一個值作重複的修改,因此最終的數值比1000要少。

2、圖解出現的緣由

發現並非同步執行的,因而我就懷疑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鎖在Spring事務管理下,爲啥還線程不安全?

 

調用方法前開啓事務,調用方法後提交事務

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

 

在多線程環境下,就可能會出現:方法執行完了(synchronized代碼塊執行完了),事務還沒提交,別的線程能夠進入被synchronized修飾的方法,再讀取的時候,讀到的是還沒提交事務的數據,這個數據不是最新的,因此就出現了這個問題。

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

 

3、解決問題

從上面咱們能夠發現,問題所在是由於@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個線程跑起來比以前要慢得多,固然咱們的數據是正確的:

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

 

最後

能夠發現的是,雖說Spring事務用起來咱們是很是方便的,但若是不瞭解一些Spring事務的細節,不少時候出現Bug了就百思不得其解。仍是得繼續加油努力呀~~~

若是想學習Java工程化、高性能及分佈式、深刻淺出。微服務、Spring,MyBatis,Netty源碼分析的朋友能夠加個人Java高級交流:787707172,羣裏有阿里大牛直播講解技術,以及Java大型互聯網技術的視頻免費分享給你們。  

相關文章
相關標籤/搜索