文章目錄
- 難點1:synchronize
- synchronized 的底層實現
- 鎖的具體操作
- 舉例說明
- 結論
- 難點2:動態代理和@Transactional失效問題
- `@Transactional` 工作原理
- 關鍵點
- 示例分析
- 正確的使用方式
- 結論
- 建議
難點所在代碼塊
@Overridepublic Result seckillVoucher(Long voucherId) {
// 1.查詢優惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判斷秒殺是否開始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {return fail("秒殺尚未開始");}
// 3.判斷秒殺是否結束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return fail("秒殺已經結束");}
// 4.判斷庫存是否充足if (voucher.getStock()<1) {return fail("庫存不足");}Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()){//避免線程安全問題,確保每次5返回的都是"5",而不是不一樣的5.toString()//如果直接return createVoucherOrder(voucherId);拿到的是當前VoucherOrderServiceImpl對象,而不是他的代理對象//注意seckillVoucher方法沒有加@Transactional,會導致如果創建訂單失敗,則不會回滾,所以需要加@Transactional//事務要想生效,是Spring對VoucherOrderServiceImpl做了動態代理,拿到了他的代理對象,用createVoucherOrder(voucherId);做的事務處理//直接return createVoucherOrder(voucherId);等于直接return this.createVoucherOrder(voucherId);指的是非代理對象//也就是目標對象,也就是沒有事務功能// 所以我們要拿到事務代理的對象才行IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//拿到當前對象的代理對象return proxy.createVoucherOrder(voucherId);//這里我們直接用代理對象proxy.createVoucherOrder(voucherId);,而不是直接this.createVoucherOrder(voucherId);
// 這樣proxy就是由Spring創建的,proxy就是由Spring管理的,就可以帶上事務功能
// 這么寫代碼還要在Spring啟動類加上@EnableAspectJAutoProxy(proxyTargetClass = true)}
// 7.返回訂單id}
這里是一個項目難點,在于為什么要userId.toString().inter(),直接userId.toString()不行么?原因在于如果是不同的線程來了,都是一個userId,那么每次toString()都是全新的String對象,JVM不是使用了equals方法去判斷是不是同一個共享資源,而是采用了對象頭信息去判斷(來自于ChatGPT-4o的回答)。
synchronized
關鍵字在 Java 中用于實現同步塊或方法,以確保多線程環境下對共享資源的訪問是線程安全的。它通過在進入同步塊或方法時獲取對象鎖(也稱為監視器鎖)來實現這一點。
難點1:synchronize
synchronized 的底層實現
synchronized
的底層實現依賴于 JVM 的內部機制。具體來說,它使用對象頭(Object Header)中的鎖信息來判斷鎖的對象是不是同一個對象。下面是一些關鍵點:
-
對象頭(Object Header):
- 每個 Java 對象在內存中都有一個對象頭。對象頭中包含了對象的元數據,包括鎖的信息。
- 對象頭的結構因 JVM 實現而異,但通常包含一個 Mark Word,用于存儲鎖標志位、哈希碼、GC 信息等。
-
鎖的狀態:
- Java 中的鎖有多種狀態,如無鎖(Unlocked)、偏向鎖(Biased Lock)、輕量級鎖(Lightweight Lock)、重量級鎖(Heavyweight Lock)。
- 鎖的狀態轉換和鎖的獲取釋放都是通過對象頭中的 Mark Word 來管理的。
-
鎖的判斷:
- 當一個線程進入同步塊或方法時,JVM 會嘗試獲取對象頭中的鎖。如果對象已經被其他線程鎖定,當前線程會被阻塞,直到鎖被釋放。
- JVM 通過比較對象頭中的 Mark Word 來判斷鎖的對象是不是同一個對象。如果兩個同步塊的鎖對象是同一個對象,那么它們的對象頭的 Mark Word 應該相同。
鎖的具體操作
當線程執行 synchronized (obj)
時:
-
鎖的獲取:
- 檢查對象頭中的 Mark Word 以確定當前鎖的狀態。
- 如果對象是無鎖狀態,JVM 會嘗試使用 CAS(Compare And Swap)操作將對象頭的 Mark Word 更新為當前線程的鎖信息,表示當前線程獲得了該對象的鎖。
- 如果對象已經被鎖定,JVM 會根據鎖的狀態(如偏向鎖、輕量級鎖、重量級鎖)執行相應的鎖獲取邏輯,可能會導致線程阻塞或自旋。
-
鎖的釋放:
- 當線程退出同步塊或方法時,JVM 會更新對象頭中的 Mark Word 以釋放鎖。
- 如果有其他線程在等待該鎖,JVM 會通知等待線程重新嘗試獲取鎖。
舉例說明
synchronized (userId.toString().intern()) {// critical section
}
在這段代碼中:
userId.toString().intern()
返回一個字符串對象的引用。synchronized
塊 會嘗試獲取該字符串對象的鎖。- 對象頭 中的 Mark Word 會記錄當前線程對該對象的鎖定信息。
- 當另一個線程執行相同的
synchronized
塊時,JVM 會檢查該字符串對象的 Mark Word,發現它已被鎖定,從而進行相應的處理(如阻塞或自旋)。
結論
synchronized
通過對象頭中的 Mark Word 來判斷鎖的對象是否相同,并通過鎖狀態的轉換和管理來實現線程同步。這確保了多個線程在訪問同一個共享資源時,不會出現線程安全問題。因此如果是"5".toString()的話,這里舉的例子是userId=5,那么每次toString出來的String對象都是不一樣的,我們的Mark Word也是不一樣的,因此無法保證是同一個共享資源,因此需要添加intern()方法,保證直接引用的是常量池里面的內容!
難點2:動態代理和@Transactional失效問題
抽取上面代碼塊關鍵兩行代碼進行解讀:
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//拿到當前對象的代理對象
return proxy.createVoucherOrder(voucherId);//這里我們直接用代理對象proxy.createVoucherOrder(voucherId);,而不是直接
VoucherOrderServiceImpl的createVoucherOrder方法是@Transactional的,但是VoucherOrderServiceImpl的seckillVoucher方法是沒有@Transactional的,如果沒有@Transactional的方法調用了@Transactional的方法,會導致@Transactional方法失效,如果發生意外是無法回滾的,
如果直接return createVoucherOrder(voucherId);拿到的是當前VoucherOrderServiceImpl對象,而不是他的代理對象
注意seckillVoucher方法沒有加@Transactional,會導致如果創建訂單失敗,則不會回滾,所以需要加@Transactional
事務要想生效,是Spring對VoucherOrderServiceImpl做了動態代理,拿到了他的代理對象,用createVoucherOrder(voucherId);做的事務處理
直接return createVoucherOrder(voucherId);等于直接return this.createVoucherOrder(voucherId);指的是非代理對象
也就是目標對象,也就是沒有事務功能
所以我們要拿到事務代理的對象才行
在 Spring 框架中,@Transactional
注解用于聲明式事務管理。了解 @Transactional
的工作原理和調用關系對于確保事務管理有效至關重要。
@Transactional
工作原理
Spring 的事務管理是通過 AOP(面向方面編程)實現的。@Transactional
注解的實現依賴于 Spring 的事務代理機制。代理對象會在方法調用前后進行事務的開啟、提交或回滾操作。
關鍵點
- 代理對象:
@Transactional
依賴于 Spring 創建的代理對象來管理事務。當一個事務方法被調用時,實際上調用的是代理對象的方法,代理對象負責處理事務邏輯。 - 方法內部調用:如果一個非事務方法調用另一個標記了
@Transactional
的方法(在同一個類中),由于是直接調用,沒有經過代理對象,事務不會生效。這是因為代理對象只能攔截外部調用,而無法攔截類內部的自調用。
示例分析
假設有以下類:
@Service
public class MyService {@Transactionalpublic void transactionalMethod() {// transactional code}public void nonTransactionalMethod() {transactionalMethod();}
}
在這個例子中:
- 如果外部類(或外部對象)調用
nonTransactionalMethod()
,事務管理不會生效,因為nonTransactionalMethod()
直接調用了transactionalMethod()
,繞過了代理對象。 - 如果外部類(或外部對象)直接調用
transactionalMethod()
,事務管理會生效,因為調用通過了代理對象。
正確的使用方式
為了確保事務管理有效,可以采取以下策略:
-
外部調用:
確保事務方法通過代理對象調用。可以將事務方法放在另一個類中,從外部調用它們。 -
自調用解決方案:
使用@Autowired
注入當前類的代理對象,確保內部調用也通過代理對象完成。@Service public class MyService {@Autowiredprivate MyService self;@Transactionalpublic void transactionalMethod() {// transactional code}public void nonTransactionalMethod() {self.transactionalMethod(); // 使用代理對象調用事務方法} }
結論
-
非事務方法調用事務方法:
- 事務管理不會生效,因為調用沒有經過代理對象。
- 這種情況下
@Transactional
注解會失效。
-
事務方法調用事務方法:
- 事務管理會生效,但前提是調用通過代理對象進行。
- 如果事務方法內部調用另一個事務方法,必須確保調用通過代理對象,否則事務管理仍然可能失效。
建議
為了確保 @Transactional
注解有效,盡量通過外部類或注入的代理對象來調用事務方法,避免在同一個類內部直接調用帶有 @Transactional
注解的方法。