本單元做業,由簡到難地實現了三版目的選層電梯。本單元主要學習java多線程的編程方法。java
難度逐漸加難,對線程安全性的要求也愈來愈高,下面分別分析這三次做業的設計,性能與線程安全處理。算法
第一次做業要求實現一部多線程的先來先服務電梯,目的是學習多線程編程語法。此次做業難度相對簡單,我採用了生產消費模型,請求隊列採用阻塞隊列實現,避免了本身實現鎖,提高了線程安全性。編程
RequestReceiver類是生產者,負責像請求隊列中添加請求,PersonRequestBox類將jar包中的請求類包裝起來,能夠添加結束域實現線程的終止。 Elevator類是消費者,從請求隊列中取出請求並執行。因爲本次做業實現的算法是先來先服務,即FIFO,能夠考慮用隊列的數據結構,加之線程安全性的考慮,我採用了BlockingQueue來實現,不須要本身實現請求隊列的阻塞了,因爲請求人數的不肯定性,我採用了LinkedBlockingQueue來實現。具體的類間關係圖以下。安全
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123608785-1622202358.png" width = "400" div align=center /> <img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421134358669-215072662.png" width = "200" div align=center />數據結構
線程的結束方法,生產者線程在接收到文件結束符以後構造一個結束域置位的PersonRequestBox放入請求隊列,時候生產者線程可結束。當消費者接收到這個特殊的請求時表示以後沒有請求了,消費者線程結束。多線程
第二次做業要求實現一部多線程的可捎帶電梯,因爲測評的緣由,我徹底按照指導書寫了一個ALS算法。常見的調度算法有Scan算法,Look算法等,我以爲吧其實ALS算法雖然在強測中性能分最低,可是這個算法的實現難度其實比較高,可以更好地練習到面向對象的多線程編程。函數
本次做業基本沿用上次做業的生產消費模型,生產者爲 RequestReceiver,消費者爲Elevator。此次的請求隊列不能再是一個隊列了,由於要捎帶,不能用僅支持FIFO的容器,因此我採用了Vector類做爲請求隊列,經過手動加鎖的方式實現線程安全。具體的類間關係圖以下。性能
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421122910229-632605023.png" width = "400" div align=center />學習
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123228575-172259041.png" width = "200" div align=center />測試
本次新加入調度器類Controller,這個類來控制請求隊列。因此整個類都須要考慮線程安全問題。因爲調度器只有一個,我採用單例模式使用Controller,並將Controller中因此操做Vector的方法synchronize住。這樣便實現了線程安全。調度器並不是線程,生產類和電梯類調用controller單例對象的方法來添加和取出請求。
第三次做業要實現多部多限制多線程可捎帶電梯,此次做業我改進了ALS算法,採用當前孤立最優算法選擇電梯運行方向,從而選擇主請求,代碼結構複用第二次做業。
因爲三部電梯可停靠樓層不一樣,致使每一個電梯可處理不一樣請求,且某些請求須要兩部電梯合做換乘才能知足,由此我根據不一樣電梯可否知足請求,構造了八個請求隊列,分別爲A、B、C、AB、AC、BC、ABC、都不能運。以A電梯爲例,A的隊列其實是A、AB、AC、ABC,這樣就能夠轉換爲第二次做業的一個隊列了。
對於須要換乘的請求,採用固定的方式將請求拆分,前一部電梯會將請求運到他能運到的最遠處,再讓後一部電梯去接應。這要求請求是鏈式可擴展的,我將PersonRequestBox類中增添了nextRequest域,達到了鏈式存儲的效果。
線程安全問題的解決和上次基本相同,將Controller類設置爲單例模式,而後將其中操控請求隊列的方法synchrnize住。下面是具體的類間關係圖。
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123957910-1217313846.png" width = "600" div align=center />
<img src="https://img2018.cnblogs.com/blog/1389272/201904/1389272-20190421123828557-603072523.png" width = "250" div align=center />
我在第三次做業中對ALS算法進行了優化,達到了不錯的性能效果。解決的問題有如下兩點
對於ALS算法來講,電梯會在內部無人的時候從請求隊列中選取主請求。因爲可捎帶,實際上主請求的選擇能夠等價於選擇電梯接下來的運行方向,主請求天然就是該方向上最早遇到的請求了。
那麼如何選擇方向呢?我採起的算法是當前孤立最優算法,所謂當前最優就是不考慮之後的請求到來狀況,孤立就是不考慮與其餘電梯搶任務的問題,只考慮這一部電梯幹完目前出現的全部請求是先向上走快仍是先向下走快。
約定
設當前電梯所在樓層爲O,O層上方共有m條請求,其中有$m_1$條要向上走,$m_2$條要向下走,O層下方共有n條請求,其中有$n_1$條要向上走,$n_2$條要向下走。$m/n_{1/2} f/t$表示指令的from樓層或to樓層,例如$m_1f$表示O上方的要向上走的請求的from樓層,其它同理。
電梯先向上運行
需向上走到的最遠樓層爲$max(m_1t, m_2f)$即$M_{up}$
以後再下到下方最遠樓層爲$min(m_2t)$和$min(n_2t, n_1f)$中較小者即$min(M_{dn}, N_{dn})$
最後將$n$指令中要向上走的人運到目的地,最遠要走到的樓層爲$max(n_1t)$記爲$N_{up}$
因此電梯走的路徑爲$O\rightarrow M_{up}\rightarrow min(M_{dn}, N_{dn})\rightarrow N_{up}$
電梯先向下運行
需向下走到的最遠樓層爲$min(n_2t, n_1f)$即$N_{dn}$
以後再上到方最遠樓層爲$max(n_1t)$和$max(m_1t, m_2f)$中較大者即$max(M_{up}, N_{up})$
最後將$m$指令中要向下走的人運到目的地,最遠要走到的樓層爲$min(m_2t)$記爲$M_{dn}$
因此電梯走的路徑爲$O\rightarrow N_{dn}\rightarrow max(M_{up}, N_{up})\rightarrow M_{dn}$
選取
經過計算以上兩種狀況電梯須要走的樓層總數,選擇總數小的方向,並將該方向上將遇到的第一條指令做爲主請求。
當一箇中轉請求到來時,必須前後依靠兩個電梯的配合才能完成。這是一個同步問題,不能再前一部電梯沒放人的時候就讓後一部電梯接走人,也就是說必須當前一部電梯到達中轉樓層後才能將後半條請求發給後一部電梯。這就產生了一個可優化的點,若是前一部電梯到達中轉樓層後,後一部電梯纔開始跑向中轉樓層,有時將浪費大量的時間。因此能夠充分利用前一部電梯運乘客的時間讓後一部電梯先到達中轉樓層。我稱之爲中轉配合法。
在Controller類內設置三個中轉樓層域,當來中轉請求後,解析中轉樓層,存入對應的中轉樓層域。當某電梯閒下來要wait時,先讀一下中轉樓層域,若是不爲空,則能夠"閒逛」(hang out)到中轉樓層,在hang out過程當中若是來了剛需請求,直接去執行剛需請求。不然就在中轉樓層wait,達到了中轉配合的目的。
關於多線程的測試,我採用了手動邊緣測試和自動壓力測試兩種方式。
自動測試經過腳本生成數據,並檢查輸出是否正確。在此吐槽一下,互測基本上是面向測評機的,由於多線程的程序,互測怎麼看八份代碼,oo真練寫腳本:)
多線程電梯是生產消費模型的經典實例,讓請求做爲生產者,電梯做爲消費者,調度器維護請求隊列。調度器要線程安全,保證請求隊列知足伯恩斯坦條件。
生產消費模型的核心是托盤類,我用Controller類來實現,第二次做業中的Controller類採用以下公有方法:
public synchronized void addList(PersonRequestBox pb); public synchronized PersonRequestBox peekMainRequest(); public synchronized PersonRequestBox getMainRequest(); public synchronized boolean isPickable(int floor, boolean direction); public synchronized List<PersonRequestBox> getPopPickList(int floor, boolean direction);
其中addList方法用來向隊列中添加請求,peekMainRequest和getMainRequest是提供電梯對主請求的操做,一個是查看但不取出主請求,另外一個是從隊列取出並刪除主請求。isPickable和getPopPickList是提供電梯對捎帶請求對操做,isPickable經過遍歷請求隊列,返回是否本層有可捎帶的請求,getPopPickList將本層可捎帶的請求從隊列中刪除並返回。
第三次做業我採用了8個請求隊列的方式,將第二次做業的Controller類改名爲ControlList,負責管理一個請求隊列,而Controller類中包含8個ControlList對象,管理8個請求隊列。Controller中的公有方法以下:
public synchronized void addList(PersonRequestBox pb); public synchronized PersonRequestBox getMainRequest(int id, int f); public synchronized PersonRequestBox takeMainRequestS(int id, int floor, boolean direction); public synchronized boolean isPickable(int floor, boolean direction, int id, int maxNum); public synchronized List<PersonRequestBox> getPopPickList(int floor, boolean direction, int id, int maxNum);
和第二次做業的方法功能基本相同,addList用來添加請求,其要解析請求並將其添加到對應的請求隊列中,其中的中轉請求要被拆分紅鏈式請求存到前一部電梯的請求隊列中。getMainRequest和takeMainRequestS兩個方法提供電梯對主請求對操做,前者經過當前孤立最優算法找到並返回主請求,後者是爲了解決電梯在獲得同層反向主請求時開關兩次門的問題而設置的一個原子操做函數,在第二次做業中由於只有一部電梯因此這個功能不須要原子性,本次中多個電梯可能會搶公共隊列中的請求,因此必須用一個原子的方法解決這個問題。最後的isPickable和getPopPickList函數在第二次的基礎上還要考慮電梯中的人數限制。
本次做業中的調度器Controller類只須要且只能有一個實例,因此將其定義爲單例模式很知足安全性,由於假如本身或其餘人在使用時將電梯類和生產者類採用了不一樣的調度器對象,將產生錯誤的結果,因此單例模式的設計是合理的。
public class Controller { private Vector<PersonRequestBox> requestList; private static Controller controller = new Controller(); private Controller() { requestList = new Vector<>(); } public static synchronized Controller getController() { return controller; } }
由於程序運行一開始就須要加載調度器,因此直接採用餓漢式在一開始就構造調度器對象。調用者經過getController方法得到調度器對象的引用,這樣就能夠經過該引用使用Controller類的方法了。
整體來講,這三次做業中學到了java多線程程序的編寫方法,掌握了保護線程安全性應用到的基本方法,包括jdk實現的線程安全類如BlockingQueue和本身用對象鎖synchronize來實現本身的線程安全類。但遺憾的是,第二次做業受指導書規則影響過分謹慎,後來規則更改後對我採起的決策徹底不利,雖然沒有bug,但性能分比較低,反正學到了東西就好吧,也但願oo課能在咱們的共同努力下愈來愈完善。
最後想說一點我對於6系oo課的想法,由於我舍友在軟件學院修oo課,他們的課程實驗讓他們學到了不少oo語法,像內部類,Lock類這種,他們是經過實驗報告的方式讓你們學習這些知識的,而咱們的oo課更注重實現功能,互相測bug,致使咱們在寫完這些做業後仍是不會用甚至不知道有這些語法。課程的目的是學知識,若是咱們比他們噁心那麼多的oo課尚未他們學的東西多範圍廣,那咱們oo課的意義何在呢?