OO第二单元作业总结
一、第二单元设计策略分析
1.1整体架构设计
第二单元作业最终是要完成多部多线程可稍带调度电梯的模拟。基于课程对于同学们迭代式开发的期望,本单元第一次作业是要求实现一部可稍带电梯(将ASL电梯作为性能参照)的模拟;第二次作业要求实现多部目的选层电梯,楼层数增加,限制了载人数;第三次作业实现多部不同类型的目的选层电梯的模拟,并且线程途中可创建电梯,要求实现换乘的功能等。
在本单元的作业中,我大体上是按照迭代式开发根据前一次作业进行扩展功能,架构设计上没有进行大的重构,但也根据性能的需要做了局部的优化和调整。
-
第一次作业
-
线程类:
InputClass 输入线程,发送请求
MainClass 主线程
Elevator 电梯线程
-
共享类(线程安全类):
Dispatcher 调度器
-
一些工具方法: 楼层映射
floorToIndex
, 用于将楼层映射为索引
本次作业采用了生产者-消费者模式,
Dispatcher
对象相当于Tray
,InputClass
是生产者,负责生产请求并发送请求至共享对象Dispatcher;笼统来说,Dispatcher
中存储了请求队列,根据电梯调度算法直接控制电梯的运动开关门。由于指导书中推荐的ASL调度算法我觉得实现很麻烦而且质疑性能并不是最好的,于是我采用了其他的电梯调度算法
(后来发现类似于第二次和第三次指导书中推荐的目的选层电梯),调度器和电梯线程的设计也与很多人不同,具体请先看下文对调度器和电梯线程策略的描述。Dispather调度器
调度器中设计了两个请求队列, 包括请求进队列和请求出队列,分别代表请求进电梯的乘客和在电梯里请求出电梯的乘客;这样一来,当
InputClass
发出请求,则进入请求进队列;当乘客进入电梯,则出请求进队列,进入请求出队列;当乘客出电梯,则出请求出队列,该请求完全满足(当然这指没有换乘的情况下)。还设计了两个电梯选层面板,请求进面板和请求出面板;其中请求进队列和请求进面板是一一对应的,若楼层x有乘客等待进入电梯,那么请求进面板相应楼层置1,否则置0,请求出同理。这样设计的目的时方便调度器根据请求队列的情况控制电梯的运行,减少重复的信息提取过程。
Elevator电梯主体部分伪代码(关注运行时,忽略了线程结束)
while(true){ //每一层循环相当于到达新的一层
while(请求队列为空){
wait();
}
Dispatcher.设置电梯运行方向;
if(是否移动){
if(向上) 向上移动一层;
if(向下) 向下移动一层;
arrive();
}
if(是否开门){
开门;
更新请求队列;
关门;
}
}由此观之,总体架构设计呼之欲出。
InputClass
负责发出请求,将请求存入Dispatcher
的请求进队列;电梯线程运行过程中需要根据调度器的请求队列情况以及设计的电梯调度算法来设置电梯的运行方向、是否上一层/下一层/不动,是否开门/关门/上下客……线程安全退出:
什么时候线程需要退出?
InputClass
: User输入请求结束时线程结束;Elevator:InputClass
线程结束,且对应调度器中请求队列无请求时线程结束;MainClass:InputClass
和`Elevator
线程结束后退出。因此我采用了信号灯方式,为了确保Elevator在
`InputClass
退出之后退出,我在共享类中设置了一个static volitale boolean InputClassRunning
,当InputClass
线程退出时,置InputClassRunning
为false,在判断请求队列为空的while循环里检测这个值,若为false,说明输入类结束了&&请求队列无请求,则return;反之,则wait , 当然此处要注意线程安全。最后为了更清晰的说明,时序图如下:
-
-
总的来说,由于对ASL可稍带调度电梯性能的质疑(我总是认为分为主请求和捎带请求没有必要,设计复杂性能优势好像也没有体现,既然电梯是每运行一层就必须输出到达信息,而且没有载客量的限制,对性能的要求也仅限于运行时间,为什么请求队列的设计和调度器的设计不能也基于楼层呢),所以第一次作业的指导书对我帮助不大,我基本上是基于目的选层的方式进行架构设计,花费了很久的时间,但是这也使得之后两次的设计工作量小了很多,实现了迭代开发。
-
第二次作业
第二次作业功能扩展为多部电梯,势必需要进行请求的分配,于是在第一次作业的架构上,增加了
Distributor
类,负责请求在电梯之间的分配,相当于一级调度器,与电梯线程解耦;而Dispatcher
相当于二级调度器,直接控制电梯线程的运行。具体分配策略见下文。此外第二次作业还对载客量有限制,需要在
Dispatcher
中设置如下变量进行载客数的监测:稍微修改一下第一次作业的更新请求队列的方法即可,即先下后上,上客时若
numLoading == capacity
,则不再有新的乘客进入。为了更清晰的说明,时序图如下:
-
第三次作业
第三次作业增加了电梯的类型,增加了换乘的需求。在第二次作业的基础上,电梯类型只需要引入工厂模式通过传入不同参数创建不同类型的电梯。为了满足换乘的需求,我这里采取了一种静态换乘策略。换乘功能在Distributor类中实现,预先将需要换乘的请求拆分成两个请求
req1和req2,
这里显然需要保证req1
出请求出队列的时刻先于req2
进入请求进队列的时刻。为此,我在Distributor
中增设了buffer
缓冲区。当请求是换乘请求时,根据某种换乘策略,将该请求拆分成req1和req2,req1
投入二级调度器,req2
投入缓冲区buffer
;当req1
满足时发出信号,flush buffer
触发req2
的投放,由此实现换乘功能,当然这里也需要注意线程安全,避免线程之间的冲突。为了更清晰的说明,时序图如下:
1.2电梯调度策略
-
最短寻找楼层时间优先算法(SSTF)
最短寻找楼层时间优先(SSTF-Shortest Seek Time First)算法,它注重电梯寻找楼层的优化。最短寻找楼层时间优先算法选择下一个服务对象的原则是最短寻找楼层的时间。
这样请求队列中距当前能够最先到达的楼层的请求信号就是下一个服务对象。
在重载荷的情况下,最短寻找楼层时间优先算法的平均响应时间较短,但响应时间的方差较大,原因是队列中的某些请求可能长时间得不到响应,出现所谓的“饿死”现象。
-
扫描算法(SCAN)
扫描算法(SCAN) 是一种按照楼层顺序依次服务请求,它让电梯在最底层和最顶层之间连续往返运行,在运行过程中响应处在于电梯运行方向相同的各楼层上的请求。
它进行寻找楼层的优化,效率比较高,但它是一个非实时算法。扫描算法较好地解决了电梯移动的问题,在这个算法中,每个电梯响应乘客请求使乘客获得服务的次序是由其发出请求的乘客的位置与当前电梯位置之间的距离来决定的。
所有的与电梯运行方向相同的乘客的请求在一次电向上运行或向下运行的过程中完成,免去了电梯频繁的来回移动。
扫描算法的平均响应时间比最短寻找楼层时间优先算法长,但是响应时间方差比最短寻找楼层时间优先算法小,从统计学角度来讲,扫描算法要比最短寻找楼层时间优先算法稳定。
-
LOOK 算法
LOOK 算法是扫描算法(SCAN)的一种改进。对LOOK算法而言,电梯同样在最底层和最顶层之间运行。**
但当 LOOK 算法发现电梯所移动的方向上不再有请求时立即改变运行方向,而扫描算法则需要移动到最底层或者最顶层时才改变运行方向。
-
第一次作业
第一次作业时原本打算采用Look算法,电梯运动前确定电梯运行方向,每次确定电梯运行方向时,判断两边是否有请求,若上层有请求则UP,否则若下层有请求则DOWN,否则默认为UP。当时以为这种算法大体上是Look算法了,应该没有什么性能缺陷,于是交上去就完事了。中途想到了一个优化策略,就是记录两边最近的需求点而不是只记录是否有需求,这样就避免了优先UP的缺陷,但当时认为其实性能提升不大,于是懒于优化,结果强测性能分只有2分(
太心痛了,没想到强测都过了没有被hack也能80出头)。静下心来思考了一下才发现,我实现的简陋版Look策略其实不是Look算法,电梯并没有来回扫楼层,电梯的运行方向完全取决于当前请求队列基于楼层的分布情况,甚至是非公平性的偏向UP方向。这样可能导致,若电梯向下运行接人,只要电梯上方突然来了个人,无论电梯此时离之前的目标请求有多近电梯都会转向。另外,随着电梯向下移动,位于电梯上方的楼层数越来越多,上方有请求的概率将越来越大,电梯突然转向对于性能的影响也越来越大,这显然是个巨大的缺陷,就能解释为什么我的电梯甚至比一次只分配一个请求的电梯还要慢!所以说如果我实现了我提出的看似简单的优化策略,这个缺陷可以很大程度上被弥补!还是太懒了,自食其果了QAQ。
-
第二次和第三次作业
痛定思痛,第二次作业实现了SSTF算法。不过唯一和SSTF算法不同的是,我计算距离时会将0层考虑进去(实际上没有0层),这是一个缺陷,但只会影响性能不影响功能,于是我这次没有修复(又不去优化,懒癌犯了)。结果性能还不错。
第三次作业基本沿用了第二次作业的算法,修复了一下之前的性能缺陷,性能尚可。
-
1.3请求分配策略以及换乘策略(第三次作业)
请求分配主要是指第二次和第三次作业。第一次作业我的设计是基于楼层响应请求,输入线程将请求直接分配给电梯,电梯根据最新的电梯面板按楼层响应请求,好像没有什么分配策略。。。
-
第二次作业
第二次作业涉及了多部电梯,采取了随机分布,id取模,不患寡而患不均。原因是这样实现异常简单,不用考虑电梯的运行状态从而使得调度器和电梯之间更加解耦,避免双向关联,提高了线程安全性。况且,由于是自动评测,id的随机性确保了取模分配的随机性,所以性能还是可以的,当然肯定不是性能最好。
-
第三次作业
第三次作业涉及换乘,电梯类型不同,为此我设计了如下数据结构存储请求。
针对请求的分配策略的伪代码如下:
if(A可以满足请求R){
id取模投入A型电梯;
}else if(B可以满足请求R){
id取模投入B型电梯;
}else if(C可以满足请求R){
id取模投入C型电梯;
}else{
换乘策略;
}
考虑到A、B、C运行时间由快到慢,于是电梯分配优先级:A>B>C;
换乘策略:根据起点站start和终点站end以及上述电梯分配优先级确定换乘子请求request1和request2对应电梯,
根据计算选择了不同换乘方案(AB,BA;AC,CA;BC,CB)对应的换乘站transfer:
然后根据T(总静态运行时)=|start-transfer|*T+|transfer-start|*T
,分别计算出不同换乘站对应的T(总静态运行时),其中T(总静态运行时)代表电梯只响应该换乘请求Request时,在忽略开关门时间的情况下,从Reqeust进请求进队列到出请求出队列并且Request得到完全满足时的时间;T表示电梯移动一层的时间。取最小值对应的换乘站transfermin,从而将换乘请求拆分为request1:start ->transfermin;request2: end->transfermin
出于对换乘的考量,我在Distributor中增加了requestCache的数据结构,用于存放拆分后的第二个子请求。换乘请求被拆分成两部分之后,request1按照上述分配原则投入电梯,request2则暂存入缓冲区requestCache。当且仅当reques1被满足时才会触发request2的投入。由此只需在电梯线程中满足请求时增加一个判断是否为换乘请求,若是换乘请求则触发投放另一子请求即可。虽然不可避免地需要进行修改,但工作量还是较小,只是需要注意线程安全。
1.4优化策略及架构设计考量
第一次作业由于大失败,在第二次作业我只是采纳了第一次作业的集中式调度的思路,在实现细节上做了优化:
-
完整实现了SSTF,取代了之前有缺陷的调度策略;
-
随着更深入学习了多线程,上锁粒度细了很多,共享对象设计为线程安全类,除了synchronized还使用了读写锁、volatile和java自带的线程安全类,如
ConcurrentHashMap
,以期实现更多的并发,提高性能;
第三次作业又做了小的优化:
-
在架构设计上,采用较为合理的分配和换乘策略,同时避免了重复的计算和循环操作,换乘站尽可能地少等等
-
响应请求时,一开门就下客,一关门就上客,缩短等待时间;
其实我认为优化最重要的还是在架构设计上。架构设计不同,性能可能会有很大差异。我之所以采取上述的随机分配策略、预先换乘策略和SSTF电梯调度策略,是因为这样实现很简单不容易出错(当然也是水平有限不知道怎么采用打表和图的算法)。增设缓冲区也是受到操作系统课cache的启发,修改扩展也很方便。另外,其实分配策略有一定的随机性,但还是加入了对性能的考量,预先换乘的实现个人认为设计也比较合理,性能不会差。总的来说,就是在性能和实现难度上进行了权衡,也考虑了可扩展性的问题,避免出现极端优化但是很不自然的情况。实际结果表明,后两次作业性能得分还是不错的。
二、基于设计原则的架构设计分析(针对第三次作业)
-
SRP Single Responsibility Principle
每个类或方法都只有一个明确的职责
这一点我觉得我做的还可以,每个类的职责比较明确。
-
Open Close Principle
无需修改已有实现(close),而是通过扩展来增加新功能(open)
实现的不够好,修改了Distributor和Dispatcher
-
其他原则
调度器实现了层次化,二级调度器;每个类职责比较明确;采用了
floorToIndex和typeToIndex
显式表达所想要表达的数据或逻辑。不好的地方可能在于,Elevator类和Dispatcher类强关联,虽然每个方法不大,但Dispatcher类方法很多,是一个重类,但我觉得暂时想不出好的设计解决这个问题。另外我Dispatcher类中的一个核心方法
answerReq()
逻辑很复杂,有点二八定律的意思了。
三、基于度量的程序结构分析
-
第一次作业类图和度量分析图
Complexity metrics | 周六 | 18 4月 2020 11:42:33 CST | |
---|---|---|---|
Method | ev(G) | iv(G) | v(G) |
Dispatcher.Dispatcher() | 1 | 1 | 1 |
Dispatcher.answerReq(int) | 9 | 9 | 9 |
Dispatcher.getCapacity() | 1 | 1 | 1 |
Dispatcher.getEmpty() | 1 | 1 | 1 |
Dispatcher.getInputOver() | 1 | 1 | 1 |
Dispatcher.getReqInFloor() | 1 | 1 | 1 |
Dispatcher.getReqNum() | 1 | 1 | 1 |
Dispatcher.getReqOutFloor() | 1 | 1 | 1 |
Dispatcher.inFloor(PersonRequest) | 1 | 1 | 1 |
Dispatcher.isEmpty() | 1 | 1 | 2 |
Dispatcher.openTheDoor(int) | 1 | 1 | 1 |
Dispatcher.outFloor(PersonRequest) | 1 | 1 | 1 |
Dispatcher.setInputOver(boolean) | 1 | 1 | 1 |
Dispatcher.updateReqIn(PersonRequest) | 1 | 1 | 1 |
Elevator.Elevator(Dispatcher) | 1 | 1 | 1 |
Elevator.arrive() | 1 | 1 | 1 |
Elevator.close() | 1 | 1 | 1 |
Elevator.open() | 1 | 1 | 1 |
Elevator.run() | 5 | 8 | 12 |
Elevator.setDirection() | 1 | 1 | 7 |
InputMain.main(String[]) | 3 | 3 | 3 |
Class | OCavg | WMC | |
Dispatcher | 1.64 | 23 | |
Elevator | 3.33 | 20 | |
InputMain | 3 | 3 | |
Package | v(G)avg | v(G)tot | |
2.33 | 49 | ||
Module | v(G)avg | v(G)tot | |
Unit2Homework1 | 2.33 | 49 | |
Project | v(G)avg | v(G)tot | |
project | 2.33 | 49 | |
-
第二次作业
Complexity metrics | 周六 | 18 4月 2020 11:51:44 CST | |
---|---|---|---|
Method | ev(G) | iv(G) | v(G) |
Dispatcher.Dispatcher(char) | 1 | 1 | 1 |
Dispatcher.MostNeededOfAll() | 10 | 3 | 14 |
Dispatcher.MostNeededOfOut() | 10 | 3 | 14 |
Dispatcher.addReqOut(PersonRequest) | 1 | 1 | 2 |
Dispatcher.answerReq() | 1 | 10 | 12 |
Dispatcher.arrive() | 1 | 1 | 1 |
Dispatcher.close() | 1 | 1 | 1 |
Dispatcher.floorToIndex(int) | 3 | 1 | 5 |
Dispatcher.inFloor(PersonRequest) | 1 | 1 | 1 |
Dispatcher.isToMove() | 1 | 1 | 5 |
Dispatcher.isToWait() | 1 | 1 | 2 |
Dispatcher.modifyDirection() | 1 | 3 | 5 |
Dispatcher.open() | 1 | 1 | 1 |
Dispatcher.openTheDoor() | 2 | 2 | 2 |
Dispatcher.outFloor(PersonRequest) | 1 | 1 | 1 |
Dispatcher.updateReqIn(PersonRequest) | 1 | 2 | 2 |
Distributor.Distributor(int) | 1 | 2 | 2 |
Distributor.distribute(PersonRequest) | 1 | 1 | 1 |
Distributor.elevatorJoin() | 1 | 2 | 2 |
Distributor.elevatorStart() | 1 | 2 | 2 |
Distributor.notifyAllElevator() | 1 | 2 | 2 |
Elevator.Elevator(Dispatcher) | 1 | 1 | 1 |
Elevator.run() | 4 | 9 | 9 |
InputProcess.InputProcess(Distributor,ElevatorInput) | 1 | 1 | 1 |
InputProcess.getInputToEnd() | 1 | 1 | 1 |
InputProcess.run() | 3 | 4 | 4 |
MainClass.main(String[]) | 1 | 1 | 1 |
Class | OCavg | WMC | |
Dispatcher | 3.44 | 55 | |
Distributor | 1.8 | 9 | |
Elevator | 3.5 | 7 | |
InputProcess | 1.67 | 5 | |
MainClass | 1 | 1 | |
Package | v(G)avg | v(G)tot | |
3.52 | 95 | ||
Module | v(G)avg | v(G)tot | |
Unit2Homework2 | 3.52 | 95 | |
Project | v(G)avg | v(G)tot | |
project | 3.52 | 95 |
-
第三次作业
Complexity metrics | 周六 | 18 4月 2020 11:56:13 CST | |
---|---|---|---|
Method | ev(G) | iv(G) | v(G) |
Dispatcher.Dispatcher(String,String,Distributor) | 2 | 2 | 4 |
Dispatcher.MostNeededOfAll() | 10 | 6 | 12 |
Dispatcher.MostNeededOfOut() | 10 | 6 | 12 |
Dispatcher.addReqOut(PersonRequest) | 1 | 1 | 2 |
Dispatcher.answerReq() | 1 | 11 | 13 |
Dispatcher.arrive() | 1 | 1 | 1 |
Dispatcher.close() | 1 | 1 | 1 |
Dispatcher.floorToIndex(int) | 3 | 1 | 5 |
Dispatcher.getDoorTime() | 1 | 1 | 1 |
Dispatcher.getReadLock() | 1 | 1 | 1 |
Dispatcher.getRunOnceTime() | 1 | 1 | 1 |
Dispatcher.getWriteCondition() | 1 | 1 | 1 |
Dispatcher.getWriteLock() | 1 | 1 | 1 |
Dispatcher.inFloor(PersonRequest) | 1 | 1 | 1 |
Dispatcher.isReqOut() | 1 | 2 | 2 |
Dispatcher.isToMove() | 1 | 1 | 5 |
Dispatcher.isToWait() | 1 | 1 | 2 |
Dispatcher.modifyDirection() | 1 | 3 | 5 |
Dispatcher.open() | 1 | 1 | 1 |
Dispatcher.openTheDoor() | 2 | 4 | 4 |
Dispatcher.outFloor(PersonRequest) | 1 | 1 | 1 |
Dispatcher.updateReqIn(PersonRequest) | 1 | 2 | 2 |
DispatcherFactory.getDispatcher(String,String,Distributor) | 1 | 1 | 1 |
Distributor.Distributor() | 1 | 1 | 2 |
Distributor.cacheCastToType(String,PersonRequest) | 1 | 1 | 1 |
Distributor.castToType(String,PersonRequest) | 1 | 1 | 1 |
Distributor.createLift(ElevatorRequest) | 1 | 1 | 1 |
Distributor.distribute(PersonRequest) | 1 | 7 | 7 |
Distributor.elevatorJoin() | 1 | 2 | 2 |
Distributor.elevatorStart() | 1 | 2 | 2 |
Distributor.floorToIndex(int) | 3 | 1 | 5 |
Distributor.flushCache(int) | 1 | 2 | 2 |
Distributor.getCacheEmpty() | 1 | 1 | 1 |
Distributor.getEndLift(PersonRequest) | 4 | 3 | 4 |
Distributor.getInputToEnd() | 1 | 1 | 1 |
Distributor.getInstance() | 1 | 1 | 1 |
Distributor.getMinFloor(String,String,PersonRequest) | 2 | 8 | 11 |
Distributor.getStartLift(PersonRequest) | 4 | 3 | 4 |
Distributor.initDispatchers() | 1 | 1 | 1 |
Distributor.notifyAllDispatchers() | 1 | 3 | 3 |
Distributor.setInputToEnd(boolean) | 1 | 1 | 1 |
Distributor.strToIndex(String) | 5 | 2 | 5 |
Elevator.Elevator(Dispatcher) | 1 | 1 | 1 |
Elevator.run() | 4 | 7 | 8 |
InputClass.InputClass(Distributor) | 1 | 1 | 1 |
InputClass.run() | 3 | 6 | 6 |
MainClass.main(String[]) | 1 | 1 | 1 |
TransferR.TransferR(PersonRequest,String) | 1 | 1 | 1 |
TransferR.getPersonRequest() | 1 | 1 | 1 |
TransferR.getType() | 1 | 1 | 1 |
Class | OCavg | WMC | |
Dispatcher | 2.91 | 64 | |
DispatcherFactory | 1 | 1 | |
Distributor | 2.42 | 46 | |
Elevator | 3.5 | 7 | |
InputClass | 3 | 6 | |
MainClass | 1 | 1 | |
TransferR | 1 | 3 | |
Package | v(G)avg | v(G)tot | |
3.08 | 154 | ||
Module | v(G)avg | v(G)tot | |
Unit2Homework3 | 3.08 | 154 | |
Project | v(G)avg | v(G)tot | |
project | 3.08 | 154 |
可以看到Dispatcher
中的answerReq()
的ev(G)基本复杂度、Iv(G)模块设计复杂度、模块判定结构的复杂程度都很高,设计得很不好,我将电梯上下客都写在这个核心方法里了,实践证明我基本上90%的bug都出在这个方法。
现在想想,可以拆分方法,上、下客没有必要写在一起。
四、测试阶段的bug分析及hack策略
测试阶段的bug
第一次作业课下测试的时候bug基本出在Dispatcher
中的answerReq()
,而这个方法很重要复杂度也很高,符合二八定律,设计不好导致的,但中测、强测一把过,没有被人hack,除了性能差之外都还行。
第二次作业强测没有发现bug,但是被hack成功了一次,原因在于线程安全退出。我采用信号灯的方式退出,但最初将信号放在了输入线程中,若输入线程结束了,可能导致被回收,从而无法将信号传出去。
hack策略
不会python也没剩下时间(懒)写脚本,这一单元作业都是手动构造数据测的,没有hack到“室友”bug。
五、反思
这一单元算是多线程入门,学到了很多东西,对多线程设计原则,设计模式等也有一定的了解,能够编写多线程程序了。但是只是入门,关于锁的机制和JVM基本原理、多线程的设计模式、如何更好地线程安全并兼顾性能还有很多需要学习的地方。
但就这一次作业来讲,我对自己还不是很满意。
感觉对自己的要求不是很高,我想到了如何优化,但是却不去实现,才会导致令人失望的性能分。后面两次作业虽然分数不错,但是总是觉得还有进步的空间;拖延症有点严重了,每次作业开始做的时间不早于星期四,导致脚本就更不可能写出来了。