# 前提 首先該場景是一個酒店開房的業務。為了朋友們閱讀簡單,我把業務都簡化了。 業務:開房後會新增一條賬單,新增一條房間排期記錄,房間排期主要是為了房間使用的時間不衝突。如:賬單A,使用房間1,使用時間段為2020-06-01 12:00 - 2020-06-02 12:00 ,那麼還需要使用房間1開房的時間段則不能與賬單A的時間段衝突。 # 業務類 為了簡單起見,我把幾個實體類都簡化了。 ## 賬單類 ```java public class Bill { // 賬單號 private String serial; // 房間排期id private Integer room_schedule_id; // ...get set } ``` ## 房間類 ```java // 房間類 public class Room { private Integer id; // 房間名 private String name; // get set... } ``` ## 房間排期類 ```java import java.sql.Timestamp; public class RoomSchedule { private Integer id; // 房間id private Integer roomId; // 開始時間 private Timestamp startTime; // 結束時間 private Timestamp endTime; // ...get set } ``` # 實戰 併發實戰當然少不了Jmeter壓測工具,傳送門: [https://jmeter.apache.org/download_jmeter.cgi](https://jmeter.apache.org/download_jmeter.cgi) 為了避免有些小夥伴訪問不到官網,我上傳到了百度雲:連結:https://pan.baidu.com/s/1c9l3Ri0KzkdIkef8qtKZeA 提取碼:kjh6 ## 初次實戰(sychronized) 第一次進行併發實戰,我是首先想到`sychronized`關鍵字的。沒辦法,基礎差。程式碼如下: ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionStatus; import java.sql.Timestamp; /** * 開房業務類 */ @Service public class OpenRoomService { @Autowired DataSourceTransactionManager dataSourceTransactionManager; @Autowired TransactionDefinition transactionDefinition; public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) { // 開啟事務 TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition); try { synchronized (RoomSchedule.class) { if (isConflict(roomId, startTime, endTime)) { // throw exception } // 新增房間排期... // 新增賬單 // 提交事務 dataSourceTransactionManager.commit(transaction); } } catch (Exception e) { // 回滾事務 dataSourceTransactionManager.rollback(transaction); throw e; } } public boolean isConflict(Integer roomId, Timestamp startTime, Timestamp endTime) { // 判斷房間排期是否有衝突... } } ``` 1. `sychronized(RoomSchedule.class)`,相當於的開房業務都是序列的。不管開房間1還是房間2。都需要等待上一個執行緒執行完開房業務,後續才能執行。這並不好哦。 2. 事務必須在同步程式碼塊`sychronized`中提交,這是必須的。否則當執行緒A使用房間1開房,同步程式碼塊執行完,事務還未提交,執行緒B發現房間1的房間排期沒有衝突,那麼此時是有問題的。 **錯誤點:** 有些朋友可能會想到都是序列執行了,為什麼不把`synchronized`關鍵字寫到方法上? 首先`openRoom`方法是非靜態方法,那麼`synchronized`鎖定的就是`this`物件。而Spring中的`@Service`註解類是多例的,所以並不能把`synchronized`關鍵字新增到方法上。 ## 二次改進(等待-通知機制) 因為上面的例子當中,開房操作都是序列的。而實際情況使用**房間1**開房和**房間2**開房應該是可以並行才對。如果我們使用`synchronized(Room例項)`可以嗎?答案是不行的。 在[第三章 解決原子性問題](https://blog.csdn.net/Lin_JunSheng/article/details/105964505)當中,我講到了**使用鎖必須是不可變物件,若把可變物件作為鎖,當可變物件被修改時相當於換鎖**,這裡的鎖講的就是`synchronized`鎖定的物件,也就是**Room例項**。因為Room例項是可變物件(set方法修改例項的屬性值,說明為可變物件),所以不能使用`synchronized(Room例項)`。 在這次改進當中,我使用了[第五章 等待-通知機制](https://blog.csdn.net/Lin_JunSheng/article/details/106129453),我添加了`RoomAllocator`房間資源分配器,當開房的時候需要在`RoomAllocator`當中獲取鎖資源,獲取失敗則執行緒進入`wait()`等待狀態。當執行緒釋放鎖資源則`notiryAll()`喚醒所有等待中的執行緒。 `RoomAllocator`房間資源分配器程式碼如下: ```java import java.util.ArrayList; import java.util.List; /** * 房間資源分配器(單例類) */ public class RoomAllocator { private final static RoomAllocator instance = new RoomAllocator(); private final List lock = new ArrayList<>(); private RoomAllocator() {} /** * 獲取鎖資源 */ public synchronized void lock(Integer roomId) throws InterruptedException { // 是否有執行緒已佔用該房間資源 while (lock.contains(roomId)) { // 執行緒等待 wait(); } lock.add(roomId); } /** * 釋放鎖資源 */ public synchronized void unlock(Integer roomId) { lock.remove(roomId); // 喚醒所有執行緒 notifyAll(); } public static RoomAllocator getInstance() { return instance; } } ``` 開房業務只需要修改openRoom的方法,修改如下: ```java public void openRoom(Integer roomId, Timestamp startTime, Timestamp endTime) throws InterruptedException { RoomAllocator roomAllocator = RoomAllocator.getInstance(); // 開啟事務 TransactionStatus transaction = dataSourceTransactionManager.getTransaction(transactionDefinition); try { roomAllocator.lock(roomId); if (isConflict(roomId, startTime, endTime)) { // throw exception } // 新增房間排期... // 新增賬單 // 提交事務 dataSourceTransactionManager.commit(transaction); } catch (Exception e) { // 回滾事務 dataSourceTransactionManager.rollback(transaction); throw e; } finally { roomAllocator.unlock(roomId); } } ``` 那麼此次修改後,使用**房間1**開房和**房間2**開房就可以並行執行了。 # 總結 上面的例子可能會有其他更好的方法去解決,但是我的實力不允許我這麼做....。這個例子也是我自己在專案中搞事情搞出來的。畢竟沒有實戰經驗,只有理論,不足以學好併發。希望大家也可以在專案中搞事情[壞笑],當然不能瞎搞。 後續如果在其他場景用到了併發,也會繼續寫併發實戰的文章哦~ > 個人部落格網址: https://colablog.cn/ 如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您 ![微信公眾號](http://qiniuyun.colablog.cn/%E4%BA%8C%E7%BB%B4%E7%A0