面向對象程序設計第二單元總結
第一次
摘要:本次作業的基本目標是模擬單部多線程電梯的運行。
調度方式
-
采取生產者消費者模式:
生產者:輸入線程(InputThread)
消費者:電梯線程(Elevator)
托盤:調度器(Dispatcher)
-
雙線程模式:
線程1:輸入線程(InputThread)
線程2:電梯線程(Elevator)
調度算法
-
Night模式:
先用2s的時間上到4樓,然后找到最高有人的樓層,到達該樓層,下到1樓,在此期間,只要有人需要上電梯則攜帶,直到電梯滿。
只可能有兩種想法:從低到高運輸,或從高到低運輸。這兩種方法在人數為6的整數倍時沒有區別。
假設有7個人,分別在2-8樓,要回到一樓。
第一種方法:電梯的運轉模式是:1-7-1-8-1。
第二種方法:電梯的運轉模式是:1-8-1-2-1。
假設兩種方法開關門時間完全相同,則時間差為\(5\times2\times0.4s = 4s\)。
如果人數更多,則時間差更多。因此第二種方法完勝第一種方法。
-
Morning模式:
電梯在一樓等待,直到電梯乘滿或輸入結束,開始運行電梯,直到電梯為空。
若輸入已經結束且電梯為空,則結束線程。否則,回到一樓,由近即遠攜帶乘客。
Morning模式和Night模式的一個區別就是:當送完最后一撥人之后,不需要再回到1樓。
假設7個人分別到2-8樓。
第一種方式,先送低的,再送高的。1-7-1-8。共19層
第二種方式,先送高的,再送低的。1-8-1-2。共15層
看起來似乎第二種方式更好。
但再假設有12個人,6個人要到5樓,6個人要到20樓。
第一種方式:1-5-1-20。共27層
第二種方式:1-20-1-5。共42層。
-
Random模式:
判斷電梯有無人,如果沒有人,則將請求隊列中fromFloor離電梯所在樓層最近的人的fromFloor設為target。如果有人,則將電梯中toFloor與當前電梯位置最近的人的toFloor設為target。
電梯無人情況:找一個離電梯當前位置的人的位置作為目標位置。
電梯有人情況:送最近的一個人到達目的地,在此過程中,如果遇到有人要上電梯且運行方向和當前電梯運行方向一致時,將其捎帶。
流程圖
代碼結構
第一次作業要求實現單部多線程可捎帶電梯,代碼結構比較簡單,UML類圖如下:
第一次作業的要求比較簡單,只有一部電梯,因此在本次作業中只有5個類,其中的Person類主要是為了后續的擴展而設計的,因此主要有4個類。其中Main為主類,InputThread為輸入線程類,Elevator為電梯線程類,Dispatcher為調度器類,負責從輸入線程中獲取指令,並且將指令發放給電梯。
同步塊的設置和鎖的選擇
Dispatcher的requests需求隊列作為輸入線程和電梯線程的共享變量,需要保證線程安全。具體設置同步塊和鎖的方式如下:
Dispatcher:
public void add(Person person) {
synchronized (requests) {
this.requests.add(person);
requests.notifyAll();
}
}
}
//在random模式下判斷是否需要開門
public boolean open(int floor, boolean up) {
synchronized (requests) {
for (Person person : requests) {
if (person.getPersonRequest().getFromFloor() == floor && person.getUp() == up) {
return true;
}
}
requests.notifyAll();
}
return false;
}
//openNight:在night模式下判斷是否需要開門
public boolean openNight(int floor) {
synchronized (requests) {
for (Person person : requests) {
if (person.getPersonRequest().getFromFloor() == floor) {
return true;
}
}
requests.notifyAll();
}
return false;
}
Elevator
synchronized (dispatcher.getRequests()) {
if (this.dispatcher.getRequests().isEmpty() && !this.dispatcher.isEndOfInput()) {
try {
this.dispatcher.getRequests().wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//讓人們進入電梯的方法
public synchronized void getIn() {
if (num == capacity) {
return;
}
synchronized (dispatcher.getRequests()) {
Iterator<Person> iterator = dispatcher.getRequests().iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
PersonRequest personRequest = person.getPersonRequest();
if (personRequest.getFromFloor() == floor) {
TimableOutput.println("IN-" + personRequest.getPersonId() + "-" + floor);
this.persons.add(person);
num++;
iterator.remove();
if (num == capacity) {
return;
}
}
}
}
}
代碼復雜度
- 類復雜度
- 方法復雜度
從復雜度分析中可以看到,Elevator類中的runMorning,runNight和runRandom方法的復雜度較高,主要原因是這三個方法作為run方法的附屬方法,與它們相關聯的方法和變量較多。
Bug分析
- 自己
本次作業在強測、互測中沒有出現Bug。
-
他人
本次作業在互測中沒有查出其他同學的Bug。
第二次
摘要:本次作業要求模擬多部同型號電梯的運行,並要求能夠響應輸入數據的請求,動態增加電梯。
調度方式
-
繼續采取生產者消費者模式,但是本次作業開始為多消費者模式:
生產者:輸入線程(InputThread)
消費者:電梯線程(Elevator)
托盤:控制器(Controller)
-
多線程模式:
輸入線程(InputThread)
每個電梯為一個線程
調度算法
-
三種到達模式的單個電梯的算法與第一次作業相同。
-
不同電梯共享一個需求隊列,由Controller將需求下發給每一個電梯的調度器,每個電梯的調度器分別從需求隊列里選擇自己的需求。
流程圖
代碼結構
第二次作業要求實現三部多線程可捎帶電梯,UML類圖如下:
第二次作業的要求比第一次稍微復雜一些,但仍然比較簡單,只有三部電梯,且電梯的型號完全相同,因此相比於上一次作業,多了一個Controller類作為總控制器,還多了一個Output類保證輸出線程安全。每個電梯擁有自己的Dispatcher調度器。其中Main為主類,InputThread為輸入線程類,Elevator為電梯線程類,Dispatcher為調度器類,負責從輸入線程中獲取指令,並且將指令發放給電梯。此時Person類多出了一個私有變量:taken。當taken為true時,說明這個人已經在電梯上了,就不可以再上其他電梯了。注意此時需要保護Person類的線程安全,因為Person是所有電梯的共享對象。
同步塊的設置和鎖的選擇
與第五次作業相同,雖然有多部電梯,但是每個電梯的調度器和輸入線程之間的共享對象為電梯自己的調度器的需求隊列,因此和上一次作業幾乎相同,需要保證電梯調度器的需求隊列的線程安全。具體的鎖如下:
Dispatcher:
//在random模式下判斷是否需要開門
public boolean openRandomNotEmpty(int floor, boolean up) {
boolean need = false;
//System.out.println("dispatcher openRandom");
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (person.getPersonRequest().getFromFloor() == floor
&& person.isUp() == up && !person.isTaken()) {
//System.out.println("random need in");
need = true;
}
if (person.isTaken()) {
iterator.remove();
}
person.notifyAll();
}
}
}
return need;
}
public boolean openRandomEmpty(int floor) {
//System.out.println("dispatcher openRandom");
boolean need = false;
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
//System.out.println(person.getPersonRequest().
//getPersonId() + " " + person.isTaken());
if (person.getPersonRequest().getFromFloor() == floor
&& !person.isTaken()) {
//System.out.println("random need in");
need = true;
}
if (person.isTaken()) {
iterator.remove();
}
person.notifyAll();
}
}
requests.notifyAll();
}
return need;
}
public boolean openMorning() {
boolean need = false;
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (!person.isTaken()) {
need = true;
} else {
iterator.remove();
}
person.notifyAll();
}
}
requests.notifyAll();
}
return need;
}
//openNight:在night模式下判斷是否需要開門
public boolean openNight(int floor) {
boolean need = false;
synchronized (requests) {
Iterator<Person> iterator = requests.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (person.getPersonRequest().getFromFloor() == floor && !person.isTaken()) {
need = true;
}
if (person.isTaken()) {
iterator.remove();
}
person.notifyAll();
}
}
requests.notifyAll();
}
return need;
}
為了避免輪詢,采用wait-notifyAll模式進行編程,具體代碼如下:
if (this.persons.isEmpty() && !dispatcher.isEndOfInput()) {
synchronized (dispatcher.getRequests()) {
try {
if (dispatcher.getRequests().isEmpty()) {
dispatcher.getRequests().wait();
}
} catch (Exception e) {
;
}
for (Person person : dispatcher.getRequests()) {
synchronized (person) {
int b = Math.abs(person.getPersonRequest().getFromFloor() - this.floor);
if (b < a) {
a = b;
target = person.getPersonRequest().getFromFloor();
}
person.notifyAll();
}
}
}
代碼復雜度
- 類復雜度
- 方法復雜度
從復雜度分析中可以看到,依然是Elevator類中的runMorning,runNight和runRandom方法的復雜度較高,原因同上一次作業相同。
Bug分析
- 自己
本次作業在強測、互測中沒有出現Bug。
-
他人
在強測中測出兩位同學的Bug,一位同學是由於線程不安全導致,另一位同學是由於線程過早結束,有一部分人還沒有到達toFloor線程就全部結束了導致。(本次作業深切體會到了多線程的隨機性,同一組數據有的時候就hack中,有時候就hack不中,還有的數據在本地測試對方輸出是有問題的,交到測評機上怎么都不中)。
第三次
摘要:本次作業要求模擬多部不同型號電梯的運行。型號不同,指的是開關門速度,移動速度,限載人數,以及最重要的——可停靠樓層的不同。
調度方式
-
繼續采取生產者消費者模式,但是本次作業開始為多消費者模式:
生產者:輸入線程(InputThread)
消費者:電梯線程(Elevator)
托盤:控制器(Controller)
-
多線程模式:
輸入線程(InputThread)
每個電梯為一個線程
調度算法
-
三種到達模式的單個電梯的算法與第一次作業相同。
-
同一型號的電梯共享一個需求隊列,由Controller將需求下發給對應型號電梯的調度器(具體分配方式取決於Person的CurrentFromFloor和CurrentToFloor),每個電梯的調度器分別從需求隊列里選擇自己的需求。
-
換乘模式:
根據多次實驗,最后采取使用Person類直接計算出中間電梯層的方式,有兩種情況:一是不需要換乘的乘客,即currentTofloor與tofloor相同的乘客,另一種是需要換乘的乘客,即currentTofloor與toFloor不相等的乘客。當到達currentTofloor時,需要進行判讀那並重新賦值,核心代碼如下:
if(currentTofloor == toFloor) { taken = false; finished = true; } else { currentFromFloor = currentTofloor; currentTofloor = toFloor; taken = false; }
流程圖
代碼結構
第三次作業要求實現多部不同型號多線程可捎帶電梯,UML類圖如下:
第三次作業的要求相比第二次作業,增加為出現不同型號的電梯,它們的移動速度、可到達樓層、容量是不同的,因此存在換乘的情況。在本次作業中有7個類,其中Main為主類,InputThread為輸入線程類,Elevator為電梯線程類,Dispatcher為調度器類,負責從輸入線程中獲取指令,並且將指令發放給電梯,Person類多了一些私有變量,用來計算換乘樓層,Output類為輸出線程安全類,總體上與第二次的框架相同。
同步塊的設置和鎖的選擇
第七次作業同第六次作業相比,不僅需要保證Dispatcher的requests線程安全,Person也成了幾個線程的共享對象,Person的一些變量也會被不同的電梯線程或調度器改變,因此需要保證Person的線程安全。具體代碼如下。
public void getOut() {
if (this.persons.isEmpty()) {
//TimableOutput.println("this.persons is empty");
return;
} else {
Iterator<Person> iterator = persons.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
if (person.getCurrentToFloor() == floor) {
output.print("OUT-" + person.getId() + "-" + floor + "-" + id);
if (person.getCurrentToFloor() == person.getToFloor()) {
person.setTaken(false);
person.setFinished(true);
iterator.remove();
} else {
person.setCurrentFromFloor(person.getCurrentToFloor());
person.setCurrentToFloor(person.getToFloor());
person.setFinished(false);
person.setTaken(false);
controller.add(person, arrivePattern);
iterator.remove();
}
num--;
}
}
}
}
public synchronized void getIn() {
if (num == capacity) {
return;
}
synchronized (dispatcher.getRequests()) {
Iterator<Person> iterator = dispatcher.getRequests().iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
synchronized (person) {
if (person.getCurrentFromFloor() == floor && !person.isTaken()
&& !person.isFinished()) {
output.print("IN-" + person.getId() + "-" + floor + "-" + id);
person.setTaken(true);
this.persons.add(person);
num++;
iterator.remove();
if (num == capacity) {
dispatcher.getRequests().notifyAll();
return;
}
} else if (person.isTaken() || person.isFinished()) {
iterator.remove();
}
person.notifyAll();
}
}
dispatcher.getRequests().notifyAll();
}
}
代碼復雜度
- 類復雜度
- 方法復雜度
從復雜度分析中可以看到,因為本次作業放棄了runMorning模式的調度,將其與runRandom模式下的調度合並,因此只有runRandom和runNight的復雜度較高,原因同前兩次作業。
Bug分析
- 自己
本次作業在互測中被查出一個Bug,是由於Morning模式下CTLE導致的。查看代碼后,發現是由於錯誤的進行了notifyAll語句導致的CTLE。
-
他人
在強測中測出一位同學的Bug,是由於線程不安全導致的,這位同學可能會讓同一個人上好幾個電梯,或者生出更多電梯,但是很難hack中,在一晚上的努力下終於中了一刀(sad)。
程序可擴展性
首先,本程序能夠通過改變Elevator類的內容以及實例的數量擴展到不同數量、不同類型的電梯。第二,在面對不同模式和模式轉變的時候,只需要輸入類給控制器類傳遞信息就可以改變調度模式。在程序編寫過程中,考慮到了高內聚低耦合的設計策略,每一個類各司其職,因此當程序的需求發生變化時,不會產生“牽一發而動全身”的情況,具有良好的可擴展性。
Hack策略
-
分析自己發現別人程序bug所采用的策略
本次發現別人程序的bug采取了閱讀代碼+測評機的模式,首先使用測評機進行評測,當出現問題(如CTLE,輸出錯誤)等情況,再去閱讀對方的代碼,找到問題所在,以便構造針對性的數據進行hack。 -
列出自己所采取的測試策略及有效性
因為測評機的測試點很多,所以能夠有效發現對方程序的漏洞,但是在上傳到課程平台時經常會出現兩種情況,一是hack不中,二是hack中了,但是中到了另一位同學身上。這是由於多線程的輸出不穩定導致的。
-
分析自己采用了什么策略來發現線程安全相關的問題
閱讀代碼,找到共享變量,再檢查在使用共享變量的時候是否采取了synchronized塊進行加鎖保護。
-
分析本單元的測試策略與第一單元測試策略的差異之處
第一單元為單線程,只需要檢查輸出是否正確,采取python的庫就可以進行正確性判斷,而第二次作業為多線程作業,只有一次輸出是正確的時候是無法確定他的程序沒有問題的,因此需要對每一個測試點進行重復測試的方式去驗證這個測試點對方是否能夠通過。
心得體會
難點__線程安全
個人認為本單元作業最難處理的地方有如下三點:
- 保證線程安全,合理使用synchronized塊。
- 避免輪詢,使用wait和notifyAll。notifyAll如果在不恰切的地方使用也會導致CTLE(第三次作業有感)。
- 如何在正確的時刻結束線程,三次作業結束線程的判斷均不一樣,如果提前結束線程或由於死鎖無法結束線程是很可惜且嚴重的失誤。
- 第一次作業:在輸入結束且電梯送完需求隊列里所有人時結束線程。
- 第二次作業:在輸入結束且電梯送完自己電梯中所有人時結束自己的線程。
- 第三次作業:在輸入結束且所有人的狀態都是finished的時候結束線程。從后續來看,第三次作業結束線程的判斷標志可以沿用到第一次作業和第二次作業。
層次化設計
本人的代碼中除了對Thread類的繼承,沒有其他繼承關系,因此代碼的結構比較簡單,三次作業均選取了生產者-消費者模式,從雙線程到多線程的轉變。對於生產者-消費者模式的應用,三次作業經歷了從單消費者到多消費者的過渡,讓我更加深刻的了解了多線程的一種常用模型:生產者-消費者模型,有助於后續對於多線程項目的開發。
互測
homework7的互測是我第一次被hack,究其原因是前期的測試做的不夠充分,並且由於homework7與homework6相比代碼的更改量比較小,因此沒有想到自己可能會出現bug,因此,在后續的作業中一定要在前期進行充分的測試,盡量避免這種事情再次發生(雖然發生了也沒轍)。