這個單元是多線程設計的實踐單元,主要讓咱們運用多線程的原理與思想去完成一個模擬電梯運行的策略。從最開始的單步電梯的傻瓜式調度,到第二次做業的單步電梯的捎帶式策略,再到第三次做業的多部電梯捎帶式運行策略。一次次的難度增強,也讓咱們發現了多線程的使用規則和方法,而且在一次次的bug中更加體會到了鎖的機制,以及各類併發機智的使用規則。雖然仍是有不少的問題,可是從這個單元確實學到了不少東西。java
這個單元編程做業,我設計了兩個線程類來完成所需的請求。分別爲:輸入請求類也就是咱們的生產者類,還有一個線程就是咱們的電梯,也就是消費者類。兩個線程共享一個請求隊列。輸入請求線程是非阻塞的,它負責向請求隊列裏寫入請求,而電梯線程負責從請求隊列裏讀出請求。並執行相應的請求。python
相應的類圖以下。linux
裏面一共有三個類,其中
Quene
是咱們的共享隊列,是一個資源,供Input
和Elevator
線程去爭奪。所以Quene
裏的方法都應該寫成同步方法,這樣才能避免線程不安全的狀況發生。時序邏輯以下。git
輸入線程不斷地向Quene裏寫入請求,Elevator
不斷地從Quene
裏取出請求而且執行。當Input
讀入null結束請求時,便向Elevator
發出一個結束信號,此時Input
線程結束。Elevator
線程收到這個結束信號時,判斷Quene
是否爲空,不爲空則繼續取出任務執行完。直到Quene
也爲空時則 Elevator
線程也結束並退出。shell
sourceFile | TotalLine |
---|---|
Dispatcher.java | 3 |
Elevator.java | 99 |
Input.java | 53 |
Quene.java | 44 |
度量分析:編程
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.close() | 1.0 | 2.0 | 2.0 |
Elevator.Elevator(Quene) | 1.0 | 1.0 | 1.0 |
Elevator.go(int) | 1.0 | 2.0 | 2.0 |
Elevator.in(int) | 1.0 | 2.0 | 2.0 |
Elevator.open() | 1.0 | 1.0 | 1.0 |
Elevator.out(int) | 1.0 | 2.0 | 2.0 |
Elevator.run() | 4.0 | 5.0 | 6.0 |
Input.getFlag() | 1.0 | 1.0 | 1.0 |
Input.Input(Quene) | 1.0 | 1.0 | 1.0 |
Input.main(String[]) | 1.0 | 1.0 | 1.0 |
Input.run() | 3.0 | 3.0 | 4.0 |
Quene.getList() | 1.0 | 1.0 | 1.0 |
Quene.getNum() | 1.0 | 1.0 | 1.0 |
Quene.requestGet() | 1.0 | 2.0 | 2.0 |
Quene.write(PersonRequest) | 1.0 | 1.0 | 1.0 |
Total | 20.0 | 26.0 | 28.0 |
Average | 1.33 | 1.73 | 1.87 |
代碼分析:數組
咱們能夠看到此次代碼中,ELevator.run
的複雜度較高,這是由於在第一次多線程的過程當中沒可以把代碼模塊化,致使電梯在運行的過程當中還承擔了不少本不該該電梯來作的事情。其實在第一次做業中,因爲沒有涉及調度器,此時電梯還須要同時扮演調度起的角色,所以其複雜度會比較高。安全
代碼缺陷:數據結構
第一次多線程實驗中,尚未理解到線程wait()
和notify()
的好處與做用,所以並無使用等待喚醒機制,致使電梯進程一直在輪詢判斷Quene
裏到底有沒有新來的請求,這也致使當請求隊列位空,可是輸入進程尚未結束的時候,電梯線程一直在作無謂的動做,佔據了大量的CPU時間,這是一個很愚蠢的設計,解決方法就是引入了等待喚醒機制,具體實施在下一次實驗中。多線程
關於Solid原則
此次的做業沒有很好的遵循SOLID原則,首先電梯線程參與了向Quene
索要請求的事情,違反了SRP原則。而電梯應該只需管本身的運行,應該把請求的分配交給Dispatcher
線程來管。同時也沒有很好的踐行ISP原則,沒有實現接口。
在第六次編程中雖然仍是一部電梯,可是引入了捎帶的策略。我吸收上一次編程中違反SOLID的教訓,將
Elevator
線程與調度分開,從新設計了一個Dispatcher
調度器線程,其做用是從Quene
中獲取請求,而且根據Elevator
的運行狀態將其分配給電梯便可。而此次的電梯有一個主請求,和一個捎帶請求隊列。電梯無論怎麼得到請求,它只管按照本身的請求去執行上樓下樓,上客下客的操做。請求的分配只歸Dispatcher
來管。
電梯的數據結構以下。
private volatile int floor = 1; //初始化在1層 private volatile PersonRequest mainRequset = null;//主請求 private volatile Vector<Person subRequest = new Vector<Person(); private volatile int empty = 1;//主請求是否爲空的標誌 private volatile int inside = 0; private volatile int status = 1;//0 means down,1 means up
其中每一個電梯都有一個mainRequest
,和一個Vector類型的SubRequest
,而且每一個電梯會返回給調度器他的當前樓層,他的運行方向等信息,來讓Dispatcher
完成新的調度。
能夠看到最大的變化就是增長了一個Dispatcher
調度器類,這個類鏈接了Elevator
線程和Input
線程。它從Input
中獲取請求,而後分配給Elevator
。
Input
線程和Dispatcher
之間的時序關係從上圖可知,Input
每次來消息,寫入到Quene
中去,而後會向Dispatcher
發起一個notify信號喚醒正在等待的調度器,告訴它有新的任務來了,他能夠恢復調度。而後Dispatcher
從Quene
中去取心得請求,把它放到本身的隊列中,並完成與Elevator
的交互。以後若是沒有其餘新的請求,則它wait()
。等待心得喚醒信號。
Dispatcher
線程和Elevator
線程的時序關係圖。Elevator
線程當本身主請求爲空的時候,就進行wait()
,等待調度器線程給其分派請求。Dispcther
給其分派請求時,notify()
這個電梯,其執行本身的主請求和副請求,一旦主請求結束,而且沒副請求時,電梯notify()
調度器,讓它給本身分配新的請求。
1:Input
結束標誌爲讀到null
2:Dispatcher
結束標誌爲Input
結束而且Quene
以及本身的請求隊列所有分配完成爲空時。
3:Elevator
結束標誌爲Dispacther
結束而且本身當前的主請求以及副請求所有執行完畢。
Source File | Total Lines | Source CodeLines |
---|---|---|
Clock.java | 3 | 3 |
Dispatcher.java | 137 | 111 |
Elevator.java | 269 | 233 |
Input.java | 87 | 69 |
Person.java | 30 | 20 |
Quene.java | 51 | 33 |
Total | 577 | 469 |
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Dispatcher.check() | 1.0 | 6.0 | 6.0 |
Dispatcher.Dispatcher(Elevator,Quene) | 1.0 | 1.0 | 1.0 |
Dispatcher.getEndFlag() | 1.0 | 1.0 | 1.0 |
Dispatcher.isSubrequest(PersonRequest) | 4.0 | 6.0 | 7.0 |
Dispatcher.run() | 3.0 | 14.0 | 14.0 |
Elevator.arrive() | 1.0 | 1.0 | 1.0 |
Elevator.checkcurrentFloor() | 1.0 | 3.0 | 4.0 |
Elevator.close() | 1.0 | 1.0 | 1.0 |
Elevator.getCurrentFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getMainRequset() | 1.0 | 1.0 | 1.0 |
Elevator.getStatus() | 1.0 | 1.0 | 1.0 |
Elevator.getSubRequest() | 1.0 | 1.0 | 1.0 |
Elevator.in(int) | 1.0 | 1.0 | 1.0 |
Elevator.loopOut() | 1.0 | 6.0 | 6.0 |
Elevator.method1() | 1.0 | 9.0 | 9.0 |
Elevator.open() | 1.0 | 2.0 | 2.0 |
Elevator.out(int) | 1.0 | 1.0 | 1.0 |
Elevator.run() | 4.0 | 10.0 | 12.0 |
Elevator.setMainRequset(PersonRequest) | 1.0 | 1.0 | 1.0 |
Elevator.up(int,int) | 1.0 | 17.0 | 23.0 |
Input.getFlag() | 1.0 | 1.0 | 1.0 |
Input.Input(Quene) | 1.0 | 1.0 | 1.0 |
Input.main(String[]) | 1.0 | 1.0 | 1.0 |
Input.run() | 3.0 | 7.0 | 8.0 |
Person.getInside() | 1.0 | 1.0 | 1.0 |
Person.getRequest() | 1.0 | 1.0 | 1.0 |
Person.Person(PersonRequest) | 1.0 | 1.0 | 1.0 |
Person.setInside(int) | 1.0 | 1.0 | 1.0 |
Person.setRequest(PersonRequest) | 1.0 | 1.0 | 1.0 |
Quene.addNum() | 1.0 | 1.0 | 1.0 |
Quene.getList() | 1.0 | 1.0 | 1.0 |
Quene.getNum() | 1.0 | 1.0 | 1.0 |
Quene.remove(int) | 1.0 | 1.0 | 1.0 |
Quene.setList(Vector<PersonRequest) | 1.0 | 1.0 | 1.0 |
Quene.setNum(int) | 1.0 | 1.0 | 1.0 |
Quene.subNum() | 1.0 | 1.0 | 1.0 |
Quene.write(PersonRequest) | 1.0 | 2.0 | 2.0 |
Total | 47.0 | 108.0 | 119.0 |
Average | 1.27 | 2.92 | 3.22 |
Elevator.up()
的複雜度比較高,緣由在於電梯的運行時須要判斷每一層是否有要進出的客人,而且須要更新其樓層運行狀態,所以複雜度較高。
此次做業基本符合SOLID要求,知足SRP要求,每一個線程只作本身分內的事情,
Input
只負責輸入,Elevator
只負責根據請求來上下運行,而Dispatcher
負責從隊列裏取請求分配給電梯。每一個線程之間的耦合性很小。
1:單一性原則:添加了
Dispacther
類,使電梯線程再也不負責請求的調度。2:開放封閉原則:這點沒有很好地實現,電梯在運行的地方不少都是硬編碼,不能很好的實現擴展。
3:里氏替換原則:因沒有使用
Extends
所以不存在此問題
1:存在的問題主要是線程與線程之間的通訊太過頻繁,線程與線程之間的關聯還不夠分離。好比Input
須要和Dispatcher
交互,而Dispatcher
須要和Elevator
進行交互,頻繁的交互使程序邏輯顯得比較混亂。
2:優點,全部的地方取消了輪詢查詢,而是所有采起了wait()
,notify()
機制,減小CPU沒必要要的執行時間,很好地迎合了多線程機制的規則。
此次是第三次多線程實驗,相比較前兩次實驗此次實驗使用了三部電梯,而且每部電梯有本身不一樣的停靠樓層,不一樣的搭乘上限,不一樣的運行速度。這就不能採起以前的硬編碼模式,須要對電梯的類進行改造。
電梯類:
private volatile int floor = 1; //初始化在1層 private volatile Vector<Person taskList = new Vector<Person(); private Dispatcher dispatcher; private String name; private long uptime; private int currentPersonNum = 0; private int space; private int[] stayFloor; private volatile int freeFlag = 1;
每一個電梯有本身的當前樓層,名字,本身的請求列表,本身的姓名,運行速度,電梯內空間與當前人數等私有屬性。還有一個重要的私有屬性,就是它的停靠樓層
stayFloor
,這是個一位數組,其在每一個電梯被建立的時候初始化。
int[] a = {-3, -2, -1, 1, 15, 16, 17, 18, 19, 20}; int[] b = {-2, -1, 1, 2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; int[] c = {1, 3, 5, 7, 9, 11, 13, 15}; Elevator elevatorA = new Elevator("A", 400, 6, a); Elevator elevatorB = new Elevator("B", 500, 8, b); Elevator elevatorC = new Elevator("C", 600, 7, c);
這就初始化好了每個電梯的信息。
由於此次做業涉及換乘,所以調度策略發生改變,調度任務依舊由
Dispatcher
線程來執行。具體策略爲每來一個請求,調度器就根據現有電梯的停靠樓層判斷其是否須要換乘,若是須要換乘,則根據實際狀況將這個請求拆分紅兩個請求。例如FR0M-2-TO-3,則須要先從2-1,再從1-3,拆分紅兩個請求以後,咱們封裝一個類Person類。
private PersonRequest request1; private PersonRequest request2; private volatile int change = 0; //是否須要換乘 private volatile int inside = 0; private volatile int first = 0; private volatile int second = 0; private volatile int finishFirst = 0; //是否已經完成第一次請求,等待執行換乘請求。
每一個Person
對象記錄者本身有哪兩個請求,而且第一個請求是否已經執行完,調度器根據這個對象的信息,將其分派給相應的電梯,電梯執行完第一個請求,則將其還給調度器,讓調度器繼續將其分派給下一個電梯完成換乘任務。
public Person analyse(PersonRequest request) { Person person; int from = request.getFromFloor(); int to = request.getToFloor(); int id = request.getPersonId(); int[] listFrom = getList(from); //得到停靠from樓層的電梯 int[] listTo = getList(to); // 得到停靠to樓層的電梯 int coincide = isCoincide(listFrom, listTo); /** * public Person(PersonRequest request1, PersonRequest request2, * int change, int first, int second) */ if (coincide != -1) { // no need change person = new Person(request, null, 0, coincide, 0); } else { //須要換乘的狀況 PersonRequest[] newRequest = newTwoRequest(listFrom, listTo, request); person = new Person(newRequest[0], newRequest[1], 1, listFrom[0], listTo[0]); } return person; }
這個函數就是完成請求拆分的方法,先得到一個請求的from和to樓層,而後看哪些電梯可以到達這些樓層,若是有重合的,則不須要換乘,不然找最快的兩部電梯,到這兩部電梯重合處進行換乘。生成兩個新的請求。
再也不設定捎帶請求,電梯根據本身的請求隊列以及運行方向來進行運行狀態的調整。若是當前運行狀態向上,就會把請求隊列裏同方向的請求所有一次性處理,直到沒有同方向的或者到達頂樓。若是一個請求執行完畢,而且其是個換乘請求則將其還給
Dispatcher
,若是其不需換乘,則直接將其從列表裏移除。
時序圖基本框架與第六次做業差很少
Dispacther
和Input
的時序邏輯。不一樣的地方在於Dispatcher
線程的結束條件爲Input
線程結束,而且本身的請求隊列爲空,而且全部的電梯的請求爲空。這是由於有時候電梯運行完一個換乘請求還會把剩下的請求還給調度器,若是提早結束,換乘請求的後半段就沒辦法完成了。
Dispatcher
與Elevator
的時序邏輯交互此時的Dispatcher
不能提早結束,須得等到本身的請求隊列爲空,而且三部電梯請求隊列也爲空而且Input
線程向其發送End標識時,才能結束。
Source File | Total Lines | Source CodeLines |
---|---|---|
Clock.java | 3 | 3 |
Dispatcher.java | 291 | 246 |
Elevator.java | 320 | 276 |
Input.java | 91 | 70 |
Person.java | 61 | 45 |
Quene.java | 22 | 16 |
Total: | 788 | 656 |
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Dispatcher.analyse(PersonRequest) | 1.0 | 2.0 | 2.0 |
Dispatcher.check() | 1.0 | 6.0 | 6.0 |
Dispatcher.Dispatcher(Elevator[],Quene) | 1.0 | 1.0 | 1.0 |
Dispatcher.down(int,int,PersonRequest) | 6.0 | 10.0 | 12.0 |
Dispatcher.getEndFlag() | 1.0 | 1.0 | 1.0 |
Dispatcher.getList(int) | 4.0 | 3.0 | 5.0 |
Dispatcher.getQuene() | 1.0 | 1.0 | 1.0 |
Dispatcher.isCoincide(int[],int[]) | 4.0 | 1.0 | 4.0 |
Dispatcher.newTwoRequest(int[],int[],PersonRequest) | 7.0 | 11.0 | 13.0 |
Dispatcher.run() | 3.0 | 23.0 | 23.0 |
Elevator.arrive() | 1.0 | 1.0 | 1.0 |
Elevator.close() | 1.0 | 1.0 | 1.0 |
Elevator.compareFloor(int) | 2.0 | 1.0 | 2.0 |
Elevator.Elevator(String,int,int,int[]) | 1.0 | 1.0 | 1.0 |
Elevator.findMaxTask(int) | 1.0 | 6.0 | 6.0 |
Elevator.getCurrentFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getFreeFlag() | 1.0 | 1.0 | 1.0 |
Elevator.getIndex(int) | 3.0 | 1.0 | 3.0 |
Elevator.getStayFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getTaskList() | 1.0 | 1.0 | 1.0 |
Elevator.getUptime() | 1.0 | 1.0 | 1.0 |
Elevator.in(int) | 1.0 | 1.0 | 1.0 |
Elevator.method1() | 2.0 | 6.0 | 9.0 |
Elevator.open() | 1.0 | 2.0 | 2.0 |
Elevator.out(int) | 1.0 | 1.0 | 1.0 |
Elevator.outThisfloor() | 3.0 | 23.0 | 24.0 |
Elevator.run() | 5.0 | 8.0 | 9.0 |
Elevator.setDispatcher(Dispatcher) | 1.0 | 1.0 | 1.0 |
Elevator.setFreeFlag(int) | 1.0 | 1.0 | 1.0 |
Elevator.up(int,int) | 2.0 | 6.0 | 10.0 |
Input.getFlag() | 1.0 | 1.0 | 1.0 |
Input.Input(Quene) | 1.0 | 1.0 | 1.0 |
Input.main(String[]) | 1.0 | 1.0 | 1.0 |
Input.run() | 3.0 | 5.0 | 6.0 |
Person.getChange() | 1.0 | 1.0 | 1.0 |
Person.getFinishFirst() | 1.0 | 1.0 | 1.0 |
Person.getFirst() | 1.0 | 1.0 | 1.0 |
Person.getInside() | 1.0 | 1.0 | 1.0 |
Person.getRequest1() | 1.0 | 1.0 | 1.0 |
Person.getRequest2() | 1.0 | 1.0 | 1.0 |
Person.getSecond() | 1.0 | 1.0 | 1.0 |
Person.Person(PersonRequest,PersonRequest,int,int,int) | 1.0 | 1.0 | 1.0 |
Person.setFinishFirst(int) | 1.0 | 1.0 | 1.0 |
Person.setInside(int) | 1.0 | 1.0 | 1.0 |
Quene.getList() | 1.0 | 1.0 | 1.0 |
Quene.setList(Vector<PersonRequest) | 1.0 | 1.0 | 1.0 |
Quene.write(PersonRequest) | 1.0 | 2.0 | 2.0 |
Total | 79.0 | 146.0 | 168.0 |
Average | 1.68 | 3.11 | 3.57 |
1:單一性原則:每一個類的職責明確,
Input
只負責輸入,Elevator
只負責根據請求來上下運行,而Dispatcher
負責從隊列裏取請求分配給電梯。每一個線程之間的耦合性很小。2:開放封閉原則:實現軟編碼,能夠添加新的電梯進入到這個系統,而且能夠在原基礎上更改Dispatcher以修改調度策略。
3:里氏替換原則:因沒有使用
Extends
所以不存在此問題
結束進程的判斷失誤。當Input
進程都到null請求時,發出Input
結束信號,Elevator
讀到這個請求就結束了本身的線程。但其實隊列中還存在請求沒有執行完。
在電梯收到Input
線程結束的請求時,再判斷一下請求隊列是否還存在未完成的請求,若是沒有,則電梯線程也結束,不然繼續執行直到請求隊列爲空。
while (true) { if (Input.getFlag() == 1 && Quene.getNum() == 0) { break; }
仍然是Dispatcher
線程提早終止的問題。當Input
請求發出終止信號時,Dispatcher
去判斷Quene
是否爲空,若是爲空,則Dispatcher
也終止。但實際上調度器本身的隊列裏還有請求沒有分配給電梯執行。
在受到Input
的結束請求時,Dispatcher
增長一條對本身請求隊列是否爲空的判斷,當input請求結束,Quene
請求隊列爲空,本身的請求隊列爲空時,向電梯線程發送Dispatcher
結束的信號。
while (true) { if (quene.size() == 0 && Input.getFlag() == 1 && requsetQuene.getList().size() == 0) { endFlag = 1; break; }
電梯運行狀態的問題,電梯每次都會根據第一條請求肯定運行方向,但若是當第一條請求爲頂層,而且電梯已經滿,則電梯在20層時又會進行一次判斷,但它仍是會向上,所以這是就會出現一個死循環。
當電梯運行到頂層或最低層的時候進行判斷,改變其運行方向。
if(this.floor==stayFloor[stayFloor.length-1]){ k = -1; } if(this.floor == stayFloor[0]){ k = 1; }
這裏的判斷,當到了當前電梯的最底層時,方向向上,最高層時運行方向向下,就不會發生死循環的狀況了。
爲了模仿測評機的按時輸入,我利用python以及linux的shell寫了測試腳本,提取每條指令的時間,而後按時將指令放到管道中去,而後重定向到標準輸入,模擬了在肯定時間輸入的狀況。
解析時間:
import time f = open(r"/Users/chenzeyin/Desktop/data",'r') temp=f.readline() sleeptime = [] req = [] lastTime = 0 while temp: temp = temp.strip("\n") gg = temp.split("]") gg[0]=gg[0][1:] sleeptime.append(float(gg[0])-lastTime) lastTime = float(gg[0]) req.append(gg[1]) temp = f.readline() sleeptime =[2.3,1.3] req=["1-FROM-2-TO-5","2-FROM-3-TO-4"] for i in range(len(req)): #print(sleeptime[i]) time.sleep(sleeptime[i]) print(req[i]) f.close()
測試
import os import time path = "/Users/chenzeyin/Desktop/czy/git/Elevator3/out/production/Elevator3" def ownTest(): os.chdir(path) os.system(r"python -u /Users/chenzeyin/Downloads/timeInput.py|java -Djava.ext.dirs=/Users/chenzeyin/Downloads Input/Users/chenzeyin/Desktop/out") ownTest()
一、多線程是一個很是困難可是有意思的章節。咱們在學習多線程的時候要抓住一個大的方向,共享對象資源,當多個線程去搶奪一個資源時須要對那個對象的資源加鎖,要搞懂synchronized
的用法,而且不要盲目使用該方法,由於這會增長CPU沒必要要的開銷。
三、合理使用wait()
,notify()
機制,減小CPU不必的浪費時間。