Java併發程式設計實戰總結 (一)
阿新 • • 發佈:2020-06-06
# 前提
首先該場景是一個酒店開房的業務。為了朋友們閱讀簡單,我把業務都簡化了。
業務:開房後會新增一條賬單,新增一條房間排期記錄,房間排期主要是為了房間使用的時間不衝突。如:賬單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