Java併發編程實戰總結 (一)

3{icon} {views}

前提

首先該場景是一個酒店開房的業務。為了朋友們閱讀簡單,我把業務都簡化了。
業務:開房後會添加一條賬單,添加一條房間排期記錄,房間排期主要是為了房間使用的時間不衝突。如:賬單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/

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

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

南投搬家公司費用需注意的眉眉角角,別等搬了再說!

※教你寫出一流的銷售文案?

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!