## 18 場景集合:并發 List、Map的應用場景
## 引導語
并發 List、Map 使用最多的就是 CopyOnWriteArrayList 和 ConcurrentHashMap,在考慮 API 時,我們也無需遲疑,這兩個并發類在安全和性能方面都很好,我們都可以直接使用。
并發的場景很多,但歸根結底其實就是共享變量被多個線程同時訪問,也就是說 CopyOnWriteArrayList 或 ConcurrentHashMap 會被作為共享變量,本節我們會以流程引擎為案例,現身說法,增加一下大家的工作經驗積累。
流程引擎在實際工作中經常被使用,其主要功能就是對我們需要完成的事情,進行編排和組裝,比如在淘寶下單流程中,我們一共會執行 20 個 Spring Bean,流程引擎就可以幫助我們調起 20 個 Spring Bean,并幫助我們去執行,本文介紹的重點在于如何使用 Map + List 來設計流程引擎的數據結構,以及其中需要注意到的線程安全的問題。
### 1 嵌套 Map,簡單流程引擎
市面上有很多流程引擎,比如說 Activiti、Flowable、Camunda 等等,功能非常齊全,但我們本小節只實現一種最最簡單的流程引擎,只要能對我們需要完成的事情進行編排,并能依次的調用就行。
#### 1.1 流程引擎設計思路
我們認為每個流程都會做 4 個階段的事情,階段主要是指在整個流程中,大概可以分為幾個大的步驟,每個階段可以等同為大的步驟,分別如下:
1. 參數校驗,主要是對流程的入參數進行校驗;
2. 業務校驗,主要是對當前流程中的業務進行邏輯校驗;
3. 事務中落庫,主要把數據落庫,控制事務;
4. 事務后事件,我們在數據落庫,事務提交之后,可能會做一些其他事情,比如說發消息出來等等。
以上每個大的階段,都會做一些粒度較細的事情,比如說業務校驗,我們可能會對兩個業務對象進行校驗,那么此時業務校驗階段就會做兩件事情,每件具體的事情,我們叫做領域行為,在實際項目中,一個領域行為一般都是一個 Spring Bean。
綜上所述,流程引擎嵌套數據結構就是:流程 -> 階段 -> 領域行為,前者對應后者,都是一對一或者一對多的關系。
我們以在淘寶上買東西時,下單為例,下單指的是我們在淘寶選擇好了商品和優惠券后,點擊購買按鈕時觸發的動作。
為了方便舉例,我們假設在淘寶上買電視和電影票,在后端,會分別對應著兩個下單流程,我們畫圖示意一下:

上圖中,左右兩個黑色長方形大框代表著兩個流程,流程下面有多個階段,階段用藍色表示,每個階段下面有多個領域行為,用紅色表示。
可以看到兩個流程中,都包含有四個階段,階段都是相同的,但每個階段中的領域行為,有的相同,有的卻是特有的。
三個概念,每個概念層層嵌套,整體組裝起來,用來表示一個流程,那么這個數據結構,我們應該如何表示呢?
使用 Map + List 即可!
#### 1.2 數據結構的定義
流程的數據結構定義分成兩步:
1. 定義出階段、領域行為基礎概念;
2. 把階段、領域行為、流程概念組合起來,定義出流程的數據結構。
首先給階段定義一個枚舉,如下 StageEnum 代表流程中的階段或步驟:
```
public enum StageEnum { PARAM_VALID("PARAM_VALID", "參數校驗"), BUSINESS_VALID("BUSINESS_VALID", "業務校驗"), IN_TRANSACTION("IN_TRANSACTION", "事務中落庫"), AFTER_TRANSACTION("AFTER_TRANSACTION", "事務后事件"), ; private String code; private String desc; StageEnum(String code, String desc) { this.code = code; this.desc = desc; } }
```
領域行為我們無需定義,目前通用的技術框架都是 Spring Boot,領域行為都是 Spring Bean,為了簡單起見,我們給領域行為定義了一個接口,每個領域行為都要實現這個接口,方便我們編排,接口如下:
```
/** * 領域行為 * author wenhe * date 2019/8/11 */
public interface DomainAbilityBean { /** * 領域行為的方法入口 */ FlowContent invoke(FlowContent content); }
```
接著我們使用 Map + List 來定義流程,定義如下:
```
/** * 第一個 key 是流程的名字 * 第二個 map 的 key 是階段,為 StageEnum 枚舉,值為多個領域行為的集合 */ Map<String,Map<StageEnum,List<DomainAbilityBean>>> flowMap
```
至此,我們定義出了,簡單流程引擎的數據結構,流程引擎看著很復雜,利用 Map + List 的組合,就巧妙的定義好了。
### 2 容器初始化時,本地緩存使用
我們定義好 Map 后,我們就需要去使用他,我們使用的大體步驟為:
1. 項目啟動時,把所有的流程信息初始化到 flowMap(剛剛定義的流程的數據結構叫做 flowMap) 中去,可能是從數據庫中加載,也可能是從 xml 文件中加載;
2. 查找流程時,直接從 flowMap 中獲取即可。
#### 2.1 初始化
以上兩步最為關鍵的點就是 flowMap 必須是可以隨時訪問到的,所有我們會把 flowMap 作為共享變量使用,也就是會被 static final 關鍵字所修飾,我們首先來 mock 一下把所有信息初始化到 flowMap 中去的代碼,如下:
```
@Component public class FlowCenter { /** * flowMap 是共享變量,方便訪問,并且是 ConcurrentHashMap */ public static final Map<String, Map<StageEnum, List<DomainAbilityBean>>> flowMap = Maps.newConcurrentMap(); /** * PostConstruct 注解的意思就是 * 在容器啟動成功之后,執行 init 方法,初始化 flowMap */ @PostConstruct public void init() { // 初始化 flowMap,可能是從數據庫,或者 xml 文件中加載 map } }
```
以上代碼,關鍵地方在于三點:
1. flowMap 被 static final 修飾,是個共享變量,方便訪問;
2. flowMap 是 ConcurrentHashMap,所以我們所有的操作都無需加鎖,比如我們在 init 方法中,對 flowMap 進行初始化,就無需加鎖,因為 ConcurrentHashMap 本身已經保證了線程安全;
3. 這里我們初始化的時機是在容器啟動的時候,在實際的工作中,我們經常在容器啟動的時候,把不會經常發生變動的數據,放到類似 List、Map 這樣的共享變量中,這樣當我們頻繁要使用的時候,直接從內存中讀取即可。
#### 2.2 使用
那我們實際使用的時候,只需要告訴 flowMap 當前是那個流程的那個階段,就可以返回該流程該階段下面的所有領域行為了,我們寫了一個流程引擎使用的工具類入口,如下:
```
// 流程引擎對外的 API public class FlowStart { /** * 流程引擎開始 * * @param flowName 流程的名字 */ public void start(String flowName, FlowContent content) { invokeParamValid(flowName, content); invokeBusinessValid(flowName, content); invokeInTramsactionValid(flowName, content); invokeAfterTramsactionValid(flowName, content); } // 執行參數校驗 private void invokeParamValid(String flowName, FlowContent content) { stageInvoke(flowName, StageEnum.PARAM_VALID, content); } // 執行業務校驗 private void invokeBusinessValid(String flowName, FlowContent content) { stageInvoke(flowName, StageEnum.BUSINESS_VALID, content); }
// 執行事務中 private void invokeInTramsactionValid(String flowName, FlowContent content) { stageInvoke(flowName, StageEnum.IN_TRANSACTION, content); } // 執行事務后 private void invokeAfterTramsactionValid(String flowName, FlowContent content) { stageInvoke(flowName, StageEnum.AFTER_TRANSACTION, content); } // 批量執行 Spring Bean private void stageInvoke(String flowName, StageEnum stage, FlowContent content) { List<DomainAbilityBean> domainAbilitys = FlowCenter.flowMap.getOrDefault(flowName, Maps.newHashMap()).get(stage); if (CollectionUtils.isEmpty(domainAbilitys)) { throw new RuntimeException("找不到該流程對應的領域行為" + flowName); } for (DomainAbilityBean domainAbility : domainAbilitys) { domainAbility.invoke(content); } } }
```
從代碼中可以看到,我們在流程引擎的入口,只要根據參數校驗、業務校驗、事務中、事務后四個階段,從 flowMap 中得到領域行為的集合,然后對領域行為進行順序執行即可。
我們在使用時,直接使用上述類的 start 方法即可。
當然以上演示的流程引擎只是一個大的框架,還有很多地方需要改進的地方,比如如何找到 flowName,如何初始化 flowMap,但這些都不是本節重點,本節主要想通過流程引擎案例來說明幾點:
1. 把 List 和 Map 作為共享變量非常常見,就像咱們這種項目啟動時,從數據庫中把數據撈出來,然后封裝成 List 或 Map 的結構,這樣做的優點就是節約資源,不用每次用的時候都去查數據庫,直接從內存中獲取即可;
2. 并發場景下,我們可以放心的使用 CopyOnWriteArrayList 和 ConcurrentHashMap 兩個并發類,首先用 static final 對兩者進行修飾,使其成為共享變量,接著在寫入或者查詢的時候,無需加鎖,兩個 API 內部已經實現了加鎖的功能了;
3. 有一點需要澄清一下,就是 CopyOnWriteArrayList 和 ConcurrentHashMap 只能作為單機的共享變量,如果是分布式系統,多臺機器的情況下,這樣做不行了,需要使用分布式緩存了。
#### 3 總結
本節內容,以流程引擎為例,說明了如何使用 Map + List 的嵌套結構設計流程引擎,以及在并發情況下,如何安全的使用 List 和 Map。
本案列是高并發項目的真實案例,感興趣的同學可以在此流程引擎框架基礎上進行細節補充,實現可運行的流程引擎。
- 前言
- 第1章 基礎
- 01 開篇詞:為什么學習本專欄
- 02 String、Long 源碼解析和面試題
- 03 Java 常用關鍵字理解
- 04 Arrays、Collections、Objects 常用方法源碼解析
- 第2章 集合
- 05 ArrayList 源碼解析和設計思路
- 06 LinkedList 源碼解析
- 07 List 源碼會問哪些面試題
- 08 HashMap 源碼解析
- 09 TreeMap 和 LinkedHashMap 核心源碼解析
- 10 Map源碼會問哪些面試題
- 11 HashSet、TreeSet 源碼解析
- 12 彰顯細節:看集合源碼對我們實際工作的幫助和應用
- 13 差異對比:集合在 Java 7 和 8 有何不同和改進
- 14 簡化工作:Guava Lists Maps 實際工作運用和源碼
- 第3章 并發集合類
- 15 CopyOnWriteArrayList 源碼解析和設計思路
- 16 ConcurrentHashMap 源碼解析和設計思路
- 17 并發 List、Map源碼面試題
- 18 場景集合:并發 List、Map的應用場景
- 第4章 隊列
- 19 LinkedBlockingQueue 源碼解析
- 20 SynchronousQueue 源碼解析
- 21 DelayQueue 源碼解析
- 22 ArrayBlockingQueue 源碼解析
- 23 隊列在源碼方面的面試題
- 24 舉一反三:隊列在 Java 其它源碼中的應用
- 25 整體設計:隊列設計思想、工作中使用場景
- 26 驚嘆面試官:由淺入深手寫隊列
- 第5章 線程
- 27 Thread 源碼解析
- 28 Future、ExecutorService 源碼解析
- 29 押寶線程源碼面試題
- 第6章 鎖
- 30 AbstractQueuedSynchronizer 源碼解析(上)
- 31 AbstractQueuedSynchronizer 源碼解析(下)
- 32 ReentrantLock 源碼解析
- 33 CountDownLatch、Atomic 等其它源碼解析
- 34 只求問倒:連環相扣系列鎖面試題
- 35 經驗總結:各種鎖在工作中使用場景和細節
- 36 從容不迫:重寫鎖的設計結構和細節
- 第7章 線程池
- 37 ThreadPoolExecutor 源碼解析
- 38 線程池源碼面試題
- 39 經驗總結:不同場景,如何使用線程池
- 40 打動面試官:線程池流程編排中的運用實戰
- 第8章 Lambda 流
- 41 突破難點:如何看 Lambda 源碼
- 42 常用的 Lambda 表達式使用場景解析和應用
- 第9章 其他
- 43 ThreadLocal 源碼解析
- 44 場景實戰:ThreadLocal 在上下文傳值場景下的實踐
- 45 Socket 源碼及面試題
- 46 ServerSocket 源碼及面試題
- 47 工作實戰:Socket 結合線程池的使用
- 第10章 專欄總結
- 48 一起看過的 Java 源碼和面試真題