一. 設計策略 1. 架構設計 三個線程:電梯,調度器,主線程(輸入線程), 採用worker thread,生產者消費者模式。和同學討論,發現有的觀點認為:調度器更像是一個功能的集合,類似一個函數,不像是一個主體,而且線程越少bug一般而言越少,於是調度器不做為線程。也挺有道理。架構圖如下: 其中 ...
一. 設計策略
1. 架構設計
三個線程:電梯,調度器,主線程(輸入線程), 採用worker thread,生產者消費者模式。和同學討論,發現有的觀點認為:調度器更像是一個功能的集合,類似一個函數,不像是一個主體,而且線程越少bug一般而言越少,於是調度器不做為線程。也挺有道理。架構圖如下:
其中task使用ConcurrentHashmap實現,對其方法沒有加synchronized, request使用普通List實現, 對其public方法都加了synchronized。這兩個類作為被多個線程訪問的類,進行了同步控制。
main向request中放入請求,調度器從request中拿出請求並分配給一個電梯(放進該電梯的task處),電梯從自己的task中拿出請求並執行,car是電梯的轎廂,代表電梯內的請求。此種實現方式不是電梯去向調度器要任務,而是調度器主動分配任務給電梯。整個架構耦合度較低。
2. 調度演算法設計
將調度分解成兩個獨立的過程:分配電梯,分配到達樓層。
分配電梯的演算法如下:
其中有容量指task.size+car.size<capacity,電梯只捎帶同方向的。
分配到達樓層:儘力而為,使到達樓層儘可能靠近請求的目的樓層。
在強測中本人的調度演算法表現的還不錯,缺點是在較少請求的情況下,沒用使負載較為均衡。
3. 線程如何結束
只設一個輸入結束的全局信號,再根據各個隊列是否為空 來決定線程是否結束很容易造成死鎖,我採用對象間發送消息的方式來退出線程。main線程在輸入結束後通知scheduler線程main已結束,然後結束自己;scheduler判斷count(表示已拆分請求個數)是否為0以及main是否結束來結束自己並向電梯線程發送scheduler已結束的消息;電梯線程根據自己的任務隊列和轎廂內是否有人來決定是否結束。綜上,main退出決定了scheduler退出,scheduler退出決定了elevator退出,整個過程非常有節奏,不再存在有進程沒被喚醒或者死鎖的情況。
二. 度量分析
1. 三次作業的UML圖
2. 以第三次作業為重點,分析經典度量
類內部的複雜度: 調度器和main類比較重
類間的依賴度:不高
方法複雜度分析(僅保留值不是很低,起主要作用的方法):調度器分配電梯的函數,電梯運行時判斷所在方向是否有請求的函數比較複雜,在情理之中。
對類與方法的代碼規模進行統計:
3. 第三次作業的時序圖
4. SOLID原則
單一責任原則: 電梯負責運行,調度器負責分配請求,其他輔助類功能都單一,符合該原則。
開放封閉原則: 第一次作業擴展性不好,導致第二次作業直接重構了,重構後的架構就是上圖所示架構,第三次作業在大的方面只修改了調度器以及電梯類的少部分代碼。
里氏替換原則,依賴倒置原則,介面分離原則:程式中沒有繼承關係和介面。
三. bug分析
三次作業均未在強測中出現過bug。
評測機:寫了個python程式來模擬隨時間的輸入,用腳本構建了一鍵測試程式,測試的自動化極大地提高了開發效率。
debug: 線程內部的功能bug可通過ide來找,但一旦涉及線程間的交互,就只能使用printf了。當然,熟練使用列印日誌的方式後,速度可以比IDE更有效。在code階段就在關鍵位置,可能有bug的位置,存在複雜計算,邏輯比較混亂的地方加上列印日誌的代碼,debug階段一看日誌就能迅速定位錯誤了。強烈推薦Hansbug寫的分級日誌輸出工具:https://github.com/HansBug/debug_logger,非常好用,極大地提高了列印日誌的友好度,真正讓我領略到了輸出調試的威力。
如果多線程的測試還靠ide, 或者肉眼去看,那可能真會出現”多線程玄學“。當深刻掌握JVM的記憶體模型,共用對象的可見性和線程的同步性以及使用日誌調試後,多線程其實也跟單線程一樣,只不過稍微麻煩一些。
四. 發現別人的bug
雖然沒有互測,但測試自己程式時還是形成了一套方法。
首先對單個模塊進行功能測試,再集成測試。測試用例既要有普遍的,也要有專門針對優化演算法設計的,數量要足夠多。很多bug都是出現在電梯滿了的時候。
五. 心得體會
1. 線程安全
什麼是線程安全,如何保證安全大家都懂,我就不贅述了,在這裡談一談大家容易忽略的問題並推薦幾篇文章:
(1)從Java多線程可見性談Happens-Before原則(鏈接)
在現代操作系統上編寫併發程式時,除了要註意線程安全性(多個線程互斥訪問臨界資源)以外,還要註意多線程對共用變數的可見性,而後者往往容易被人忽略。這篇文章非常清晰地解釋了JMM(java的記憶體模型)中的happens-before原則,讀完能夠深刻透徹地理解該原則如何解決多線程對共用變數的可見性問題(包括緩存一致性和重排序)。
雖然可見性問題在這幾次作業中我並沒有遇到,但應該引起註意。
(2)關於double-checked locking的討論
雙檢查鎖這個技巧看起來精巧,但卻是醜陋的,由於JVM的優化越來越完善,在現代工程開發中DCL已經被廢棄了。
2. 設計原則
UML建模:在進行項目的時候,通過使用 UML 的面向對象圖的方式能夠更明確、清晰的表達項目中的架設思想、項目結構、執行順序等一些邏輯思維。這幾次的作業比較簡單,架構也很明晰,沒必要先畫出UML圖後再去碼代碼。儘量減少類與類之間的依賴,可以通過消息機制完成類間的通信。