2019面向對象程序設計——目的選乘電梯之優化篇java
做者:1723 🐺算法
優化前言:安全
通過了第二單元三次電梯的歷練,可能不會有特別多的人比我更加深切體會到一個優秀架構的重要性。由於在優化策略上耗費巨大心血的我,雖然強測測試點的確拿了一些100,但卻第二次電梯做業卻由於架構設計上的不足和犯懶沒作更多的自主測試,強測慘痛爆點,直奔C屋,撿了芝麻丟了西瓜。在第二次做業後,我在架構設計上進行了許多思考,而且普遍參考了許多16級和17級高工巨佬們的架構設計。在保證正確性的基礎上,優化策略的價值便體現出來,所以,本文也會介紹一些優化策略。架構
一個優秀的架構也許不足以讓你強測拿頂級分,若沒有優秀的架構,對於數據較少的強測,也許能拿頂級分,但對於大批量數據測試,性能和安全性的問題就會暴露出來。對多電梯來講,架構設計尤爲重要。機器學習
一.架構設計要點:主線程退出問題性能
這個問題和輸入線程、調度器、電梯的死亡問題不是同一個問題。學習
在第一次做業中,指導書中在「輸入接口.md」中明確指出:建議單獨開一個線程處理輸入。而在以後的第三次指導書中,則改成了:建議主線程處理輸入。我猜想,這個修改多是因爲大多數同窗在擁有了輸入線程以後,卻忽視了主線程的做用。測試
對於單開輸入線程的同窗,這個設計要點,你可能會感到詫異:個人主線程負責創造其餘線程,創造完以後,它就死了,不會形成其餘影響,也不會引發正確性的問題啊?優化
沒錯,確實可能在此次做業中不會引發正確性上的問題,甚至那些難以復現的bug也不是所以而產生,但這不符合工程規範,架構的邏輯性也存在問題。this
下面這句話,是一個優秀架構所須要的重要設計規範之一:(注:這句話不是我說的,源於一位大佬助教的分享)
保證全部非主線程的生命週期都直接或間接被主線程嚴密控制,即主線程第一個出生,最後一個死亡。
如何能保證主線程最後一個死亡?方法之一就是線程的聯合:即join()方法。
在此僅簡介,具體請本身深刻研究。
一個線程A在佔有CPU資源期間,可讓其餘線程調用join()方法和本線程聯合,例如B.join();稱A在運行期間聯合了B。若是線程A在佔有CPU資源期間聯合了B線程,那麼A線程將馬上中斷執行,一直等到它聯合的線程B執行完畢,A線程再從新排隊等待CPU資源,以便恢復執行。
以第二次做業爲例,除了主線程以外,因爲電梯線程是最後死亡的,因此主線程在執行最後一條語句以前聯合電梯線程,在電梯線程死亡後再死亡。
//建立調度器線程 Dispatch dispatch = new Dispatch(eleNum, inputDeadFlag, dispatchDeadFlag, upReqList, downReqList); //建立電梯線程 Elevator elevator = new Elevator(1, dispatchDeadFlag); //建立輸入線程 InputThread inputThread = new InputThread( upReqList, downReqList, inputDeadFlag); dispatch.addElevator(elevator, 0); //start電梯線程 elevator.start(); //start調度器線程 dispatch.start(); //start輸入線程 inputThread.start(); try { //主線程聯合電梯線程 elevator.join(); } catch (InterruptedException e) { e.printStackTrace(); }
二.架構設計要點:主線程、調度器線程、電梯線程的安全死亡問題
這個問題談不上架構優化,是全部同窗都必須解決好的一個問題,可是你們的解決方法各不相同。有些原始方法在第2、三次做業中可能會遇到線程安全問題,好比在第三次做業中,直接將 null put到請求隊列中,電梯讀到null自動終止的方法有多是不安全的(只是對於部分設計來講不安全)。
如下是一個利用同步鎖的我的推薦(並不必定真的好)的方法:
執行邏輯:
輸入線程在沒有input時終止(即讀到null終止);
調度器線程將在輸入線程終止且一級請求隊列(調度器請求隊列)沒有待分配的請求時終止;
電梯線程將在調度器終止且二級請求隊列(電梯內部請求隊列)沒有待執行請求且電梯內沒人時終止。
線程控制:
托盤對象類:
public class DeadFlag { private boolean deadFlag; DeadFlag() { this.deadFlag = false; } boolean getDeadFlag() { synchronized (this) { return this.deadFlag; } } void setDeadFlag() { synchronized (this) { this.deadFlag = true; } } }
輸入線程和調度器共享一個托盤對象;調度器和電梯共享一個托盤對象
//輸入線程死亡 try { synchronized (inputDeadFlag) { inputDeadFlag.setDeadFlag(); inputDeadFlag.notifyAll(); } } catch (Exception e) { e.printStackTrace(); }
//調度器線程死亡 if (inputDeadFlag.getDeadFlag() && upReqList.isEmpty() && downReqList.isEmpty()) { break; } //在break以後 synchronized (dispatchDeadFlag) { dispatchDeadFlag.setDeadFlag(); dispatchDeadFlag.notifyAll(); }
//電梯線程死亡 if (dispatchDeadFlag.getDeadFlag() && upReqList.isEmpty() && downReqList.isEmpty() && personIn.isEmpty()) { break alive; }
其實調度器沒有必要單開線程,能夠做爲附屬組合模塊,這樣也會大大下降了線程安全控制的難度,以上是我第二次做業寫的電梯之一,調度器線程是爲了第三次做業的複用作準備(其實可徹底能夠不用)。
三.架構設計要點:CPU時間
關於評測機的CPU超時檢測是一大謎題,但一部分僅有wait()¬ifyAll或await()&signalAll()的架構在這強測中出現了CPU連環爆點的狀況。(我在本地作過CPU測試,然而仍是被hack了CPU超時.....)
如下是三種下降CPU方法,推薦使用①+③組合或方法②
方法①:wait()+notifyAll()或利用ReentrantLock&Condition類的await()+signalAll()
ReentrantLock&Condition類的await()+signalAll()比wait()+notifyAll()更加靈活精確,推薦嘗試。同步方法與加鎖在此再也不贅述,詳情請見老師上課的課件或上網自學。
方法②(推薦):電梯自主自殺與調度器激活法
這一方法雖然暴力,而且沒太用到互斥控制,但通過測試,這種方法下降CPU時間上有奇效,推薦使用(要求使用Runnable接口)。
何謂電梯自主自殺?
答:使用Runnable接口建立的電梯線程在執行完全部請求後自動死亡。
何謂調度器激活?
答:調度器每分發一個請求給電梯後,會調用isAlive()方法檢查電梯線程是否alive,若是電梯線程已經死亡,則從新電梯建立一個線程並激活。
具體實現
public class Dispatcher { //初始化,每個elevator對應一個thread,起初都是null。 LinkedList<Elevator> elevators; LinkedList<Thread> threads = new LinkedList<>(); public Dispatcher(LinkedList<Elevator> elevators) { this.elevators = elevators; for (int i = 0; i < elevators.size(); i++) { threads.add(null); } } //剩餘部分省略 }
//調度器先把請求加進i號電梯。 getElevators().get(i).addRequest(request); //檢查i號電梯對應的線程狀態,如果死的線程則爲電梯從新建立線程並激活。 if (getThreads().get(i) == null || !getThreads().get(i).isAlive()) { getThreads().set(i,new Thread(getElevators().get(i))); getThreads().get(i).start(); }
方法③:電梯線程sleep(1)
強烈建議想使用方法①,而且把請求分給全部電梯讓電梯自由搶的同窗,再使用方法③做爲輔助方案。
爲何多電梯使用wait()與notifyAll()仍會炸點?我認爲緣由有二:
其一:電梯wait()的機會少。
其二:電梯剛wait()就被喚醒。這樣電梯會很生氣
若是是電梯搶請求,請求分給多個電梯,甚至同一請求按照不一樣拆分方式分給電梯,電梯請求隊列的請求數目較多,基本不會休息,這樣一來,大量線程爭奪CPU資源,爆點也在乎料之中。
當咱們設置sleep時,等於告訴CPU,當前的線程再也不運行,持有當前對象的鎖。那麼這個時候CPU就會切換到另外的線程了,所以讓電梯線程sleep(1)也是能緩解CPU時間的一個輔助方法。
四.頂級架構模式:調度策略與電梯運行的徹底抽象剝離
調度策略與電梯運行相剝離的抽象剝離的架構,是頂級設計架構之一,但我在作做業時死🐟安樂並無去嘗試,只能過後諸葛,我係計算機專業的wsz巨佬基本實現了這一架構,如下設計參考wsz同窗的思路。
何謂調度策略與電梯運行的徹底抽象剝離?
答:電梯只負責上下樓,調度策略徹底由外部決定
只有不到10行的電梯類!基類Base也只有100行不到,只負責上下樓和安置Strategy。
public class Elevator extends Base { public Elevator() { super(); setStrategy(new Strategy()); } }
Strategy是電梯的調度策略,是外部類,可直接安置給電梯。
爲何被普遍承認爲頂級架構?
答:你能夠把各類sao操做,奇技淫巧,優化策略分別寫成一個單獨的Strategy類,電梯想換策略直接換一個Strategy類就能夠,極大地與Solid原則吻合。
對於不一樣的Strategy,其基礎功能也能夠定義接口
public interface Strategy { LinkedList<Job> getFinishJobs(int curFloor); void addJob(PersonRequest request); String getDirection(int curFloor); boolean isEmpty(); }
若想多策略並行擇優選擇,這一架構很是合適
五.單電梯優化策略:LOOK算法+條件折返
只談性能,用戶體驗爲0
LOOK算法的平均運行時間會明顯優於純貪心算法以及先來先服務的ALS算法。使用LOOK算法,強測基本以及能夠拿到90的成績,但LOOK算法自己有性能上的弊端,即不管人怎麼懟門,它也不折返。處理好什麼時候該折返、什麼時候不折返問題,強測數據點甚至能夠拿一半以上的滿分。
好比:
233-FROM-1-TO-4 2333-FROM-3-TO-4 23333-FROM-8-TO-13
這樣的測試點,ALS電梯會明顯優於LOOK電梯,若是不加處理,LOOK會優化分爆零。
若是你的電梯沿運行方向已經走過了某個樓層,卻有生氣的乘客瘋狂懟門,你開不開門呢?如下提出兩種優化觀點,一爲模擬,二爲預測。
模擬:
看到有dalao已經實現了多策略電梯,即每一個請求給多個電梯都跑一遍,擇優輸出,這樣的電梯性能想必是極強無比的。在這裏,我介紹另一種優化觀點:模擬。
對於個人LOOK電梯,每作一個方向上的任務時,有三個屬性:
private EleStatus status; private EleStatus dirc; //注:初始折返次數:0,最大折返次數:1 private int backtrack;
status表示的是電梯想處理哪一個方向的一趟請求,dirc是電梯當前運行方向,backtrack記錄折返次數
電梯每到一層樓時,若是status與dirc是同向的(dirc與status不一樣向時意味着電梯正在反向去接最上或最下樓層的請求,接完後開始作任務),且有須要折返的請求時,若這趟任務的折返次數爲0,則進行模擬折返和模擬不折返,提早暴力算出完成當前所有請求,折返與不折返的時間開銷,不容許一趟請求屢次折返。如何計算並不困難,可直接暴力拿電梯開關門時間,電梯運行一層時間莽算便可。
//用於模擬折返,返回折返稍人的完成所有請求的總運行時間 private int simulateBack(int curFloor);
//用於模擬不折返,返回不折返的完成所有請求的總運行時間 private int simulateNotBack(int curFloor);
以上行舉個例子
//條件1 if (dirc.equals(EleStatus.up) && status.equals(EleStatus.up))
//條件2 遍歷上行請求隊列發現存在request知足request.getFromFloor()<curFloor(當前樓層)
//條件3 this.backtrack = 0;
知足以上請求則進行模擬
顯然電梯折返的運行狀況是:
status:up->down dirc:down->up->down
不折返的運行狀況是:
status:up->down->up dirc:up->down->up
統計代表:電梯折返佔優與不折返的佔優數據測試爲三七開,要想拿那3成的優化分,模擬是很重要的。
預測:
我沒這麼幹,也沒能力讓電梯機器學習
一個會機器學習的電梯於本次做業是無用的,由於強測數據點很隨機。
對於大規模有必定規律性數據的測試,這也與平時生活相似。電梯能夠根據實時統計結果或累計統計結果動態調整策略,達到在空乘狀態下預測將來請求分佈從而優化性能的目的。
有興趣的同窗們能夠在電梯單元結束以後嘗試一下,(反正我懶,摸了)。
六.多電梯優化策略
1.全拆分
通過聯合測試,發現原本指望度極高的圖算法拆分在大量隨機數據面前卻敗下陣來。緣由估計是電梯運行環境太過於複雜,換乘僅進行一次拆分的圖算法雖然自己沒有任何問題,但仍是難以預料電梯到底是怎麼走的。但本次強測孰優孰劣卻是很差說,數據點不少是[0.0]或者其餘時間點集中投放,或者間隔極短投放,這時電梯處於起步狀態,運行是可預料的,單一拆分就會有優點。
通過詢問,採用下面這種方法的同窗也都所有91+,不失爲一種不錯的策略,而我,本次做業換乘只進行了一次盲目自信的豪賭拆分,並沒拿到很高的優化分,哭遼。
①若是無需換乘,這我的把A, B, C電梯都摁一遍
②若是須要換乘,先儘可能在相同方向上拆分請求,但全部電梯組合(A&B、B&A、A&C、C&A、B&C、C&B)以及每種電梯組合的所有拆分可能都拆一遍,而且所有投入電梯,達到最混沌的狀態。
好比請求1-FROM--3-TO-2對於A,B電梯組合會被分紅: 1-FROM--3-TO--2(A) + 1-FROM--2-TO-2(B) 1-FROM--3-TO--1(A) + 1-FROM--1-TO-2(B) 1-FROM--3-TO-1(A) + 1-FROM-1-TO-2(B) 所有加進請求隊列
//A,B電梯的上行請求拆分爲例 floorsS = floorA floorsE = floorB; if (judgeIn(floorsS, fromFloor) & judgeIn(floorsE, toFloor)) { for (int p = 0; p < floorsS.length; p++) { for (int q = 0; q < floorsE.length; q++) { if (floorsS[p] == floorsE[q] && floorsS[p] > fromFloor && floorsE[q] < toFloor) { int[] req1 = {id, fromFloor, floorsS[p], i, j, 1}; int[] req2 = {id, floorsE[q], toFloor, j, j, 0}; request.add(req1); request.add(req2); flag = true; } } } }
③若是沒法同向換乘,則反向拆分到距離請求樓層最近的樓層。
④一個電梯拿到請求,須要把其餘電梯請求隊列中id相同且非下一階段的請求remove掉。
全拆分自己並非一種強大的算法優化,而只是一種減小平均損失的折中策略,它之因此能在強測中佔重要一席之地,是由於電梯運行的隨機性和數據點的隨機性形成各種算法運行時間的不穩定性,甚至單一拆分的電梯搶人的運行時間仍然不夠穩定,減少平均損失的折中可能不會拿到頂級分,但也會拿到很不錯的優化分(91+)。
2.請求分配策略
原則:①不徹底平攤。快的電梯仍是應該多拿請求的,畢竟運行速度擺在那,但決不能鴿(🕊)了電梯C。
②不集中。也儘量不能讓電梯一次吃足。
爲何要提出這樣的問題?
下面分析這樣一個情景:
假設在1樓,B電梯吃飽了請求,C可能只吃了不多的請求。
B電梯拍拍肚子往2樓跑,這時有一個id=2333的人忽然懟門
2333-FROM-2-TO-3
A:不去
C:不去
B:飽了不去,等下波吧
id=2333的人:當場暴斃
若是B電梯把C電梯可共享的請求多分擔一部分,就能夠搭上這個請求了。
因此說,一個合理的調度,除了換乘以外,應當是電梯速度+負載狀態的選擇。
七.總結
2019年OO電梯單元的設計可謂獨出心裁,我在其中獲得了諸多歷練,以上的優化點只是九牛一毛,更多的但願你們不吝分享。總之:架構重要,架構重要,架構重要(哭唧唧)。