OO第5-7次做業總結

OO第5-7次做業總結

這三次做業所有是關於電梯的。主要鍛鍊了多線程編程的能力,以及瞭解一些調度算法的使用。html

[TOC]java

一、設計分析

1.一、單電梯FCFS調度方案

第五次做業,不是通常的簡單,幾乎人人滿分的那種。dalao們甚至在30行內寫出單線程調度就解決了。摸魚划水的一次。本着練習多線程的目的,我仍是老老實實寫了調度。只有一個請求隊列,因爲不考慮捎帶,因此直接FIFO隊列便可。因爲當時對同步和鎖的掌握很淺,爲了求穩,對於同步訪問(隊列爲空時等待)採用了輪詢的辦法。這樣雖避免了死鎖,但浪費CPU時間。正則表達式

架構:首先定義兩個線程(輸入、電梯),它們在主線程中啓動,共享一個請求隊列。這隊列用線程安全容器LinkedBlockingQueue。一方面是爲了阻塞,另外一方面是爲了保證隊列長度(即size()方法)的訪問是一個原子操做。另外定義一個狀態類WhenToStop,用來指示輸入是否結束。stopInputting()方法用來通知兩個線程輸入已結束。算法

電梯的移動,是直接到目標樓層的,不須要考慮某一層掉頭等的問題。編程

UML類圖安全

image

複雜度分析多線程

Methods:架構

Method ev(G) iv(G) v(G)
Elevator::Constructor 1 1 1
Elevator.absSub(int,int) 2 1 2
Elevator.closeDoor() 1 3 3
Elevator.getOff() 1 1 1
Elevator.getOn() 1 1 1
Elevator.openDoor() 1 3 3
Elevator.run() 2 3 4
Elevator.toFloor(int) 1 3 3
InputThread::Constructor 1 1 1
InputThread.run() 1 4 4
Main.main(String[]) 1 1 1
WhenToStop.isInputting() 1 1 1
WhenToStop.stopInputting() 1 1 1
Total 15.0 24.0 26.0
Average 1.15 1.85 2.0

Classes:併發

Class OCavg WMC
Elevator 1.625 13.0
InputThread 1.5 3.0
Main 1.0 1.0
WhenToStop 1.0 2.0
Total 19.0
Average 1.46 4.75

耦合度分析ide

Class Cyclic Dcy Dcy* Dpt Dpt*
Elevator 0 1 1 1 1
InputThread 0 1 1 1 1
Main 0 3 3 0 0
WhenToStop 0 0 0 3 3
Average 0.0 1.25 1.25 1.25 1.25

複雜度控制仍是比較好的,沒有一個方法的複雜度超過5。耦合度也不高。線程間通訊只靠兩個共享對象。

時序圖(線程間通訊機制)

image

1.二、單電梯可捎帶

此次須要可捎帶,因此第五次做業那種只設置一個共享隊列的方法不適用了。由於捎帶請求必須同時考慮電梯外請求和電梯內乘客,並在合適時機掉頭。

我查了一下可行的調度算法,包括:scan算法,look算法,ALS,還有FCFS,最近距離等

scan算法是上下循環調度,相似於擺渡車、輪渡,缺點是沒有乘客的樓層也要停。

look是scan的改進,沒有乘客和請求的樓層不停,直接掉頭。

ALS和FCFS太熟悉了,不用說。

還有一種是最近距離,也就是當電梯內有乘客時,乘客優先。無乘客時,距離當前樓層最近的請求優先。但這會出現某些請求「餓死」的狀況。

綜合以上幾種,仍是look比較好。

基本思想就是,請求要分爲電梯外請求和電梯內乘客兩部分。電梯外請求按照出發樓層分類,電梯內請求按照目標樓層分類。每當電梯到達某一層時,檢查是否須要上客或下客。下客的標準是到達目標樓層,上客的標準是他的請求方向和電梯當前運行方向(不是主請求的請求方向)相同。有則開門,無則甩過直接走。每當準備移動到下一層時,檢查是否須要掉頭(檢查是否掉頭是一個掃描過程,若是電梯內有乘客則不能掉頭,不然掃描電梯外請求,若是當前方向上沒有請求則掉頭,若是兩邊都沒有請求,則停下來等待)。

此次因爲是單部電梯,因此直接複用上次的架構,不須要增長調度盤。線程安全上,考慮了用lockcodition來進行同步、互斥。這樣靈活性比較高。

所用容器以下(直接粘貼Main類源碼,省略import):

public class Main {
    public static void main(String[] args) {
        TimableOutput.initStartTimestamp();

        final ReentrantLock reentrantLock = new ReentrantLock(); // 鎖和條件
        final Condition condition = reentrantLock.newCondition();

        final HashMap<Integer, LinkedBlockingQueue<PersonRequest>> outerRequests
                = new HashMap<>(); // 電梯外請求
        for (int i = -2; i <= 16; i++) { // 這裏的樓層作了特殊處理,以便電梯能連續運行,輸出時再轉換
            outerRequests.put(i, new LinkedBlockingQueue<>());
        } // 每層的請求都要初始化一個空隊列
        final InputtingState state = new InputtingState(); // 指示輸入是否完成

        InputThread inputThread = new InputThread(outerRequests,
                reentrantLock, condition, state); // 輸入線程
        Elevator elevator = new Elevator(outerRequests,
                reentrantLock, condition, state); // 電梯線程

        elevator.start();
        inputThread.start();
    }
}

電梯內乘客的容器只在Elevator類中定義和使用,不須要共享。容器類型和電梯外請求相同。

UML類圖

image

複雜度分析

Methods

Method ev(G) iv(G) v(G)
Elevator::Constructor 1 2 2
Elevator.checkDirection() 10 6 10
Elevator.closeDoor() 1 4 4
Elevator.downOneFloor() 1 3 3
Elevator.getFloor() 2 1 2
Elevator.getOff() 1 2 2
Elevator.getOn() 3 5 5
Elevator.isEmpty(HashMap<Integer, LinkedBlockingQueue<PersonRequest>>) 3 2 3
Elevator.moveOneFloor() 1 2 2
Elevator.openDoor() 1 3 3
Elevator.realFloor(int) 2 1 2
Elevator.run() 3 9 10
Elevator.upOneFloor() 1 3 3
InputThread::Constructor 1 1 1
InputThread.run() 1 4 4
InputtingState.isInputting() 1 1 1
InputtingState.stopInputting() 1 1 1
Main.main(String[]) 1 2 2
Total 35.0 52.0 60.0
Average 1.94 2.89 3.33

Classes

Class OCavg WMC
Elevator 3.08 40
InputThread 1.5 3
InputtingState 1 2
Main 2 2
Total 47.0
Average 2.61 11.75

可見檢查掉頭的方法checkDirection()的複雜度仍是比較高的,遠遠超過平均複雜度。這個方法要綜合考慮乘客和外請求,代碼量也比較大,達到了30行。另外,Elevator類是實現核心功能的,可是包含了調度方法,因此複雜度也比較高。當時是爲了方便,調度器和請求隊列合一。

耦合度分析

Class Cyclic Dcy Dcy* Dpt Dpt*
Elevator 0 1 1 2 2
InputThread 0 2 2 1 1
InputtingState 0 0 0 3 3
Main 0 3 3 0 0
Average 0.0 1.5 1.5 1.5 1.5

耦合度依然不高,由於兩個線程通訊仍然只須要兩個共享對象、一把鎖、一個條件(監聽器)。

時序圖(線程間通訊機制)

image

1.三、三部電梯協同調度

此次又不同了,不只增長了兩部電梯,並且增長了乘客數量限制、停靠層限制。因此,這決定咱們須要一個調度盤來把乘客請求分配到合適的電梯上去。而且,考慮換乘問題。

電梯類統一建模,可是增長一些屬性,包括停靠層限制、核載、運行速度。利用(半)工廠模式,構造函數只傳入電梯類型,根據電梯類型來決定這些屬性的值。因爲仍然採用look算法,因此能夠複用上次的電梯,只須要對電梯運動狀況和掉頭的條件作一些調整。

架構就是三個電梯、一個調度盤,請求隊列分三塊,一塊是原始請求,也就是剛輸入的時候,沒有通過調度器分配,能夠理解爲站在大廳門外。一塊是分配後請求,能夠理解爲站在某個電梯門口。最後是電梯內乘客。

換乘不難解決,爲了一勞永逸的防止出錯,咱們把未完成的請求扔回原始請求中,也就是回滾。這樣保證各個電梯互相獨立。電梯只負責運送乘客,無論任何調度問題。這樣保證耦合度低,不易出錯。

另外的坑點,注意中止條件。這一次,使用共享對象unfinishNum(自定義一個類SyncInteger,模擬原子整數),表示未完成的請求個數(從一個請求產生到它從電梯中出去並上到目標樓層爲止,這段時間稱做未完成)。當輸入結束,而且未完成請求個數爲0時,全部線程所有終止。再有就是注意輸出互斥問題,由於輸出函數對stdout是競爭關係,因此同一時刻只能有一個線程在輸出,不然會致使輸出穿插(這個穿插是指行內穿插)混亂。

三部電梯的調度,應該讓它們儘可能並行,這一步由調度器Dispatcher來完成。調度器也是一個線程,和輸入線程、電梯線程併發。具體的調度優化,詳見:https://www.cnblogs.com/wancong3/p/10739633.html

UML類圖

image

複雜度分析

Methods

Method ev(G) iv(G) v(G)
Dispatcher::Constructor 1 1 1
Dispatcher.dispatch(PersonRequest,int) 3 4 4
Dispatcher.fixedFloor(int,int,int) 13 4 15
Dispatcher.run() 3 11 12
Elevator::Constructor 2 7 10
Elevator.checkDirection() 10 9 13
Elevator.closeDoor() 1 4 4
Elevator.downOneFloor() 1 3 3
Elevator.fixedFloor(PersonRequest) 12 4 14
Elevator.getFloor() 2 1 2
Elevator.getOff() 1 4 4
Elevator.getOn() 5 6 10
Elevator.isEmpty(HashMap<Integer, LinkedBlockingQueue<PersonRequest>>) 3 3 4
Elevator.openDoor() 1 3 3
Elevator.realFloor(int) 2 1 2
Elevator.run() 3 11 12
Elevator.syncOutput(String) 1 1 1
Elevator.toNextFloor() 1 3 3
Elevator.upOneFloor() 1 3 3
InputThread::Constructor 1 1 1
InputThread.run() 1 4 4
Main.main(String[]) 1 7 7
SyncInteger.SyncInteger(int) 1 1 1
SyncInteger.getValue(int) 1 1 3
ThreadRunning.isRunning() 1 1 1
ThreadRunning.stopRunning() 1 1 1
Total 73.0 99.0 138.0
Average 2.81 3.81 5.31

Classes

Class OCavg WMC
Dispatcher 5.75 23
Elevator 4.33 65
InputThread 1.5 3
Main 7 7
SyncInteger 2 4
ThreadRunning 1 2
Total 104.0
Average 4.0 17.33

複雜度較高的方法就是fixedFloor(),用於尋找合適的換乘點。它採起了隨機順序的方法,而且循環搜索。

另外值得注意的就是,調度器類和電梯類的複雜度都比較高,多是由於使用了過多的共享對象。還有就是Main類的OCavg居然達到了7,也和初始化過多的共享對象有關。可是沒有辦法啊,原本電梯換乘是乘客考慮的,強行扔給調度器也是真的懶(哈哈哈~)。

耦合度分析

Class Cyclic Dcy Dcy* Dpt Dpt*
Dispatcher 0 3 3 1 1
Elevator 0 2 2 3 3
InputThread 0 3 3 1 1
Main 0 5 5 0 0
SyncInteger 0 0 0 4 4
ThreadRunning 0 0 0 4 4
Average 0.0 2.167 2.167 2.167 2.167

遵循SOLID原則,調度器和電梯功能分離,下降耦合度仍是有效果的,我以爲不要管那些極限性能,這樣設計纔是最佳方案。系統穩定性比單個數據的性能重要得多。

SOLID原則

一、單一責任:每一個類只管本身該管的事情。我以爲這個是重中之重,電梯就是運輸乘客,調度器才負責具體哪一個乘客上哪一個電梯。這樣增長了可維護性,即便要改電梯也不會牽一髮動全身。

二、開閉控制:哪些類支持擴展,哪些類是final的,哪些函數設置爲對外接口,哪些函數是封裝的。另外,一樣的功能能夠定義成抽象方法,支持不一樣的實現。電梯其實能夠這樣作,把開關門、上下客和移動到下一層的方法做爲抽象方法。只不過,一次做業中沒有多種不一樣實現的電梯,因此目前還不必這麼作。

三、里氏替換:和自定義類的繼承(is-a繼承)有關。extends Thread不是啥設計層面的繼承,不用管。

四、依賴倒置:強調依賴關係中,高層模塊的抽象。電梯固然不會依賴本身。(但這個原則在第三次做業,就是多項式、三角函數複合求導中相當重要,由於求導方法依賴於具體的表達式形式,只有把表達式類抽象出來才能獲得語法樹)

五、接口分離:說的是,不要用單一接口實現多功能。電梯的設計,遠沒有這麼複雜。

時序圖(線程間通訊機制)

image

二、bug殺蟲和測試方法

若是按照測試的標準,這三次的強測沒有錯誤。互測呢,根本沒有針對性,很難發現bug。

來講說中測發現的bug。第五次就不說了,沒有bug。

第六次,沒提交的時候,就有嚴重的問題,停不下來。後來發現使用await()signalAll()用錯了位置,在lock()unlock()以外使用,不只沒法結束,還報一大堆異常。

第七次更好笑。我知道三樓確定會出一些小差錯,就故意輸入三樓的數據,結果上下往返停不下來。發現是換乘點有問題。再後來本身手動輸入沒問題,連文件輸入都沒測試,過於自信的提交,結果等了半分鐘不出結果,已經有點慌了。第四分鐘,不出所料,通通通通RTLE。

好吧,個人數據果真不行。借了它的兩組數據發現問題很嚴重,一部電梯停下時另兩部沒停。乾脆這樣吧,當一個電梯將要結束時,通知其它電梯也停下來(由於此時電梯中止的條件已經知足,只要一接到signal馬上中止)。

有點怕了不敢亂提交,就本身用C寫了一個數據生成器,又把官方的輸入接口反編譯了一下,本身加了定時輸入。測了二三十組吧,沒問題纔敢提交(我知道複用上次的電梯,邏輯確定不會有問題,就怕出在同步問題上)。

互測呢,感受就是象徵性地跑一跑交空刀刷活躍度。後來第六次身份公開,發現我和六系四大神仙一個組(具體是誰,我屋的人確定知道),我一打擦邊球進A組的菜雞也能享有這樣的待遇,難怪誰都發現不了問題。專門拜讀了他們的代碼,感受一些強優化仍是很是牛B的,我等算法小白甘拜下風。另外,每一個人的調度思路都不一樣,不太可能經過讀代碼來針對性出數據,只能是測一測比較容易出錯的地方,而後隨機生成幾個大量數據走壓力測試。和第一單元不同啊,第一單元徹底是語法分析,這是多線程。沒有了WF寫起來真輕鬆,互測就八臉懵逼了。

在這裏特別感謝老柴削麪、東方削麪HDL、DYJ、ZYY三位奆佬提供的定時輸入方法、Special Judge和對拍器,雖然最終沒用它們發現bug,可是至少解了燃眉之急。

多線程調試,尤爲是當死鎖的時候,最好的方法就是printf,你能夠在printf的字符串中加一個debug,記得在提交的時候逐一刪除(能夠用idea的正則表達式替換,超級方便,因此我說你寫個debug這樣的標誌)。線程安全相關問題,printf大法好!

此次測試和第一單元不一樣的地方在於,不是簡單工具能夠解決的,必須靠本身分析題目邏輯來判斷是否正確。

三、收穫

三次做業強迫我學習了多線程。其實最重要的收穫就這一點。具體點就是線程安全考慮。只要是設計併發程序,時刻都要考慮線程安全。掌握了線程安全的幾種方法:同步、條件鎖、原子類、線程安全容器。

還有就是隨機算法的認識,在數據良莠不齊或徹底隨機的狀況下,隨機算法能顯著提升平均性能。由於在實際中,最壞狀況因爲出現機率極小,每每不重要,這也是平攤分析的基本思想。

這三次做業,我感到OO通過改革比往屆好得多了,除了前兩次做業之外,已是真正的面向對象。類設計、多線程、並行編程在工程開發中仍是很是重要的。另外就是強測作得愈來愈好,互測已經不怎麼是得分手段了,從而能真正體現出程序的質量(固然,有時候也會栽,強測炸的狀況不是沒有過,若是bug修復能撿回強測部分分就更好了)。

四、工具推薦

時序圖工具:PlantUML Integration

安裝方法:(這個是IDEA插件,喜歡用Eclipse的小夥伴自行去找,反正我是沒找到)

一、IDEA-文件-設置

二、選擇Plugins選項卡,單擊上方Plugins Market

三、搜索PlantUML Integration

四、install

五、重啓IDEA

使用方法詳見:http://www.javashuo.com/article/p-uyqzdobb-ga.html

另外給你們推薦一個PlantUML詳解:https://www.cnblogs.com/Jeson2016/p/6837186.html

相關文章
相關標籤/搜索