Java併發程式設計實戰總結 (一)

Johnson木木發表於2020-06-06

前提

首先該場景是一個酒店開房的業務。為了朋友們閱讀簡單,我把業務都簡化了。
業務:開房後會新增一條賬單,新增一條房間排期記錄,房間排期主要是為了房間使用的時間不衝突。如:賬單A,使用房間1,使用時間段為2020-06-01 12:00 - 2020-06-02 12:00 ,那麼還需要使用房間1開房的時間段則不能與賬單A的時間段衝突。

業務類

為了簡單起見,我把幾個實體類都簡化了。

賬單類

public class Bill {
    // 賬單號
    private String serial;

    // 房間排期id
    private Integer room_schedule_id;
    // ...get set
}

房間類

// 房間類
public class Room {
    private Integer id;

    // 房間名
    private String name;
    // get set...
}

房間排期類

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://pan.baidu.com/s/1c9l3Ri0KzkdIkef8qtKZeA
提取碼:kjh6

初次實戰(sychronized)

第一次進行併發實戰,我是首先想到sychronized關鍵字的。沒辦法,基礎差。程式碼如下:

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例項)可以嗎?答案是不行的。
第三章 解決原子性問題當中,我講到了使用鎖必須是不可變物件,若把可變物件作為鎖,當可變物件被修改時相當於換鎖,這裡的鎖講的就是synchronized鎖定的物件,也就是Room例項。因為Room例項是可變物件(set方法修改例項的屬性值,說明為可變物件),所以不能使用synchronized(Room例項)
在這次改進當中,我使用了第五章 等待-通知機制,我新增了RoomAllocator房間資源分配器,當開房的時候需要在RoomAllocator當中獲取鎖資源,獲取失敗則執行緒進入wait()等待狀態。當執行緒釋放鎖資源則notiryAll()喚醒所有等待中的執行緒。
RoomAllocator房間資源分配器程式碼如下:

import java.util.ArrayList;
import java.util.List;

/**
 * 房間資源分配器(單例類)
 */
public class RoomAllocator {
    private final static RoomAllocator instance = new RoomAllocator();

    private final List<Integer> 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的方法,修改如下:

    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/

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您
微信公眾號

相關文章