OO第二单元总结
目录
第一次作业
1.设计策略
第一次作业要求实现一个可捎带的电梯。整体结构采用了生产者-消费者模式,电梯作为消费者,输入客户端作为生产者,二者共享同一个调度器,电梯从调度器中取走请求,客户端向调度器发送请求。调度算法我选择了SSTF算法,即每次都让离电梯最近的请求作为主请求,在完成主请求的过程中判断是否可捎带。主请求完成之后再选择新的主请求。
由于是第一次接触多线程编程,对于线程同步理解不到位,对于synchornized
关键字,wait()
, notify()
等方法的理解还非常生疏,在多次出现bug后最终给所有可能引起线程安全问题的方法加了 synchronized
关键字,不过由于本次作业只涉及一个生产者,一部电梯,对性能造成的影响不大。
2.度量分析
(1) 代码规模
第一次作业代码规模不大,总共5个类,一共276行代码
类名 | LOC |
---|---|
Elevator | 149 |
InputRequest | 37 |
MainClass | 15 |
Person | 39 |
Scheduler | 76 |
可以看见主要的代码集中再Elevator
中,Elevator完成了大部分工作。
(2) UML类图
如图所示,是一个非常简单的结构,Person类对PersonRequest进行了封装,PersonRequst作为Person的一个成员变量。电梯不直接对PersonRequest进行操作执行,这样做的好处是,可以在Penson中定义一些人的行为,比如人的上下电梯GetIn()
和GetOff()
,这样做类的分工更明确,并且考虑到未来扩展中有可能涉及到Person需要在电梯之间换乘来达到目的地,后续可扩展Person,让Person自己决定换乘策略,使得类内聚性更好。
不过本次的调度策略完全由Elevator自己实现,没有体现出调度器的作用,调度器完全变成了一个容器,这不利于程序后续的扩展。
(3) 复杂度分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(TreeMap<Integer, ArrayList |
1 | 1 | 1 |
Elevator.EndProgram() | 4 | 5 | 6 |
Elevator.GetDown() | 1 | 1 | 1 |
Elevator.GetUp() | 1 | 1 | 1 |
Elevator.MainFloor() | 8 | 3 | 10 |
Elevator.Move(int) | 1 | 4 | 4 |
Elevator.OpenCloseDoor() | 1 | 3 | 3 |
Elevator.PeopelInOff() | 1 | 4 | 4 |
Elevator.isArrived(int) | 1 | 1 | 1 |
Elevator.run() | 1 | 7 | 7 |
InputRequest.InputRequest(ArrayList |
1 | 1 | 1 |
InputRequest.add() | 3 | 4 | 4 |
InputRequest.run() | 1 | 1 | 1 |
MainClass.main(String[]) | 1 | 1 | 1 |
Person.GetIn() | 1 | 1 | 1 |
Person.GetOff() | 1 | 1 | 1 |
Person.Person(PersonRequest) | 1 | 1 | 1 |
Person.getFromFloor() | 1 | 1 | 1 |
Person.getId() | 1 | 1 | 1 |
Person.getToFloor() | 1 | 1 | 1 |
Person.isInElevator() | 1 | 1 | 1 |
Scheduler.Schedule() | 1 | 6 | 6 |
Scheduler.Scheduler() | 1 | 2 | 2 |
Scheduler.addRequest() | 1 | 5 | 5 |
Scheduler.getElevatorQueue1() | 1 | 1 | 1 |
Scheduler.getInputRequests() | 1 | 1 | 1 |
Scheduler.getInstance() | 1 | 1 | 1 |
Total | 39 | 60 | 68 |
Average | 1.44 | 2.22 | 2.52 |
可以看到MainFloor()
方法基本复杂度和圈复杂度都非常高,这一部分主要是电梯决定自己的主请求,由于Elevator中用HashMap来属于保存不同楼层的请求,因此在计算最近楼层时逻辑稍显复杂,有很多if-else
和for
的嵌套,复杂度较高
Class | OCavg | WMC |
---|---|---|
Elevator | 3.1 | 31 |
InputRequest | 1.67 | 5 |
MainClass | 1 | 1 |
Person | 1 | 7 |
Scheduler | 2.17 | 13 |
本次作业大部分工作都由Elevator来完成,因此Elevator类复杂度较高,内部包含方法也众多,某些工作其实应该交由Scheduler来完成,类之间的分工做得不太好。
(4) 协作图
第二次作业
1.设计策略
第二次作业在第一次作业的基础上增加了负楼层,并且要实现多部电梯同时运作,电梯数量由最开始给出。整体架构相对于第一次作业变化不大,借鉴Worker Thread Pattern,Scheduler中增加一个threadpool成员变量,用来管理所有的Elevator线程,线程的创建也由Scheduler来完成,每个Elevator的运作方式以及捎带策略和第一次作业相同,主请求改为由Scheduler决定。
在同步方面,改进了第一作业中无脑synchornized
的方法,对需要上锁的代码块进行了一定了缩减。
2.度量分析
(1) 代码规模
代码总长度为352行,仍然分为5个类。
类名 | LOC |
---|---|
Client | 38 |
Elevator | 196 |
MainClass | 13 |
Person | 32 |
Scheduler | 111 |
相比于第一次作业代码增量不多,Elevator和Scheduler划分更加明确。
(2) UML类图
如图所示,由于Person下电梯时需要输出电梯id,避免麻烦,减少耦合,将GetIn()
和GetOff()
移到了Elevator中,Client通过Scheduler提供的方法向Scheduler中发送请求,Elevator通过Scheduler提供的方法从中取出请求。主目标由Scheduler决定,捎带策略仍然由电梯自己完成,这样可以减少Scheduler的压力,避免电梯过多时,Scheduler计算压力太大。同时电梯线程的创建由Scheduler来完成,对于未来可能出现的动态创建Elevator线程,具有比较好的扩展性。
(3) 复杂度分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Client.Client() | 1 | 1 | 1 |
Client.CreateElevators() | 1 | 1 | 1 |
Client.run() | 3 | 4 | 4 |
Elevator.CanPickUp(Person) | 3 | 7 | 7 |
Elevator.Elevator(String) | 1 | 3 | 3 |
Elevator.GetDown() | 2 | 1 | 2 |
Elevator.GetIn(Person) | 1 | 1 | 1 |
Elevator.GetOff(Person) | 1 | 1 | 1 |
Elevator.GetUp() | 2 | 1 | 2 |
Elevator.Move(int) | 1 | 4 | 4 |
Elevator.OpenCloseDoor() | 1 | 3 | 3 |
Elevator.PeopleInOff() | 1 | 3 | 3 |
Elevator.PickUp() | 2 | 4 | 5 |
Elevator.getFloor() | 1 | 1 | 1 |
Elevator.getPersonNum() | 1 | 3 | 3 |
Elevator.isArrived() | 1 | 1 | 1 |
Elevator.isUp() | 1 | 1 | 1 |
Elevator.run() | 8 | 12 | 13 |
Elevator.setTargetFloor(int) | 1 | 1 | 1 |
Elevator.takeRequest(Person) | 1 | 1 | 1 |
MainClass.main(String[]) | 1 | 1 | 1 |
Person.Person(PersonRequest) | 1 | 1 | 1 |
Person.getFromFloor() | 1 | 1 | 1 |
Person.getId() | 1 | 1 | 1 |
Person.getToFloor() | 1 | 1 | 1 |
Person.isInElevator() | 1 | 1 | 1 |
Person.setIn() | 1 | 1 | 1 |
Scheduler.CreateElevators(int) | 1 | 2 | 2 |
Scheduler.Scheduler() | 1 | 1 | 1 |
Scheduler.StartElevators() | 1 | 2 | 2 |
Scheduler.WaitInput() | 1 | 3 | 3 |
Scheduler.getInstance() | 1 | 1 | 1 |
Scheduler.getRequestqueue() | 1 | 1 | 1 |
Scheduler.putRequest(Elevator) | 4 | 12 | 16 |
Scheduler.takeRequest(Person) | 1 | 1 | 1 |
Total | 52 | 84 | 92 |
Average | 1.49 | 2.40 | 2.63 |
从图中可以看到,复杂度最高的方法是putRequest()
,该方法用于Elevator从Scheduler中取出请求,为了从主目标所在楼层的所有请求中选出唯一的主目标,并且对可捎带的请求进行捎带,这个方法中含有大量对请求队列的循环以及距离的比较。不过由于每部电梯有了人数限制,并且因为多重循环导致的该方法开销较大,实际运行过程中并不一定能提高电梯系统的效率,有时反而增加了电梯实际运行时间(当然,多部电梯的运行时间本就有一定随机性)。
CanpickUp()
方法需要判断时候满足捎带条件,因此本身条件判断比较多,整体复杂度会略高。
run()
方法有一定的问题,有些具体的取请求策略直接在run方法中展开,导致run显得有些臃肿,不利于后续的扩展。
Class | OCavg | WMC |
---|---|---|
Client | 1.67 | 5 |
Elevator | 2.47 | 42 |
MainClass | 1 | 1 |
Person | 1 | 6 |
Scheduler | 2.62 | 21 |
此次作业电梯的工作量比较大,另外由于将主请求的分配交给Scheduler,Scheduler的复杂度也略有上升。
(4) 协作图
第三次作业
1. 设计策略
第三次作业在第二次作业基础上要求动态创建电梯线程,并且电梯分为A, B, C三种型号,每种电梯有不同的运行速度,可停靠楼层以及最大容量。相比较于前两次作业,我认为此次作业在难度上有一个较大的提升,由于不同型号电梯的属性不同,调度策略可能会非常多样且灵活。并且由于停靠楼层的限制,换乘策略也是一个值得关注的点。
在这一次的设计中,为了方便电梯的调度,我将Scheduler中仍在电梯外的请求分为A, B, C三个队列,分别对应于可以乘坐A, B, C三种不同电梯的请求。当前可以乘坐哪种电梯完全由Person自己来判断,Scheduler只需根据Person提供的相关信号量将Person放入不同请求队列中,如果电梯将乘客送到当前目的地后乘客仍未到达最终目的地,则乘客再次返回到Scheduler中相应的请求队列中。A, B, C三种电梯继承自Elevator,Elevator作为一个抽象类,每种电梯有自己不同的构造方法。其它架构仍和作业二类似。
2. 度量分析
(1) 代码规模
此次作业代码总长度为765行,相比于前两次作业有了一个显著的提升。
类名 | LOC |
---|---|
Client | 46 |
Elevator | 224 |
ElevatorA | 30 |
ElevatorB | 28 |
ElevatorC | 30 |
ElevatorFactory | 17 |
MainClass | 12 |
Person | 301 |
Scheduler | 161 |
可以看到,代码的增长除了A, B, C三种电梯类之外,最主要是Person类的代码量有了一个巨幅增长,其主要原因是,针对A, B, C三种电梯的换乘,有一系列比较复杂的判断逻辑,不过代码中部分代码重复度较高,仍有很大精简的空间。
(2) UML类图
可以看到,Client通过调用Scheduler中的方法来创建电梯线程,电梯对象的创建则由一个简单的工厂来完成。Person类中多了一些属性,isSelectX
提供给Scheduler,表示自己能乘坐哪些种类的电梯,还有多了一个中间的currentFloor
,currentTarget
,用来表示当前所在楼层,和当前目标,方便电梯进行捎带,运输。
(3) 复杂度分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Client.Client() | 1 | 1 | 1 |
Client.run() | 3 | 6 | 6 |
Elevator.CanPickUp(Person) | 3 | 7 | 7 |
Elevator.Close() | 2 | 4 | 5 |
Elevator.Elevator(String,int,int) | 1 | 1 | 1 |
Elevator.GetDown() | 2 | 1 | 2 |
Elevator.GetIn(Person) | 1 | 1 | 1 |
Elevator.GetOff(Person) | 1 | 2 | 2 |
Elevator.GetUp() | 2 | 1 | 2 |
Elevator.Move(int) | 1 | 4 | 4 |
Elevator.Open() | 2 | 2 | 3 |
Elevator.PeopleInOff() | 1 | 6 | 6 |
Elevator.addPassenger() | 1 | 1 | 1 |
Elevator.getFloor() | 1 | 1 | 1 |
Elevator.getId() | 1 | 1 | 1 |
Elevator.getPassengersNum() | 1 | 1 | 1 |
Elevator.getRequests() | 1 | 1 | 1 |
Elevator.getTargetFloor() | 1 | 1 | 1 |
Elevator.isArrived() | 1 | 1 | 1 |
Elevator.isFinished() | 1 | 1 | 1 |
Elevator.isUp() | 1 | 1 | 1 |
Elevator.run() | 1 | 11 | 11 |
Elevator.setFinished(boolean) | 1 | 1 | 1 |
Elevator.setTargetFloor(int) | 1 | 1 | 1 |
ElevatorA.ElevatorA(String) | 1 | 2 | 2 |
ElevatorA.getFloors() | 1 | 1 | 1 |
ElevatorA.getType() | 1 | 1 | 1 |
ElevatorB.ElevatorB(String) | 1 | 2 | 2 |
ElevatorB.getFloors() | 1 | 1 | 1 |
ElevatorB.getType() | 1 | 1 | 1 |
ElevatorC.ElevatorC(String) | 1 | 2 | 2 |
ElevatorC.getFloors() | 1 | 1 | 1 |
ElevatorC.getType() | 1 | 1 | 1 |
ElevatorFactory.GenerateElevator(String,String) | 5 | 2 | 5 |
MainClass.main(String[]) | 1 | 1 | 1 |
Person.ChooseElevator() | 6 | 1 | 7 |
Person.Person(PersonRequest) | 1 | 1 | 1 |
Person.getCurrentFloor() | 1 | 1 | 1 |
Person.getCurrentTarget() | 1 | 1 | 1 |
Person.getId() | 1 | 1 | 1 |
Person.getToFloor() | 1 | 1 | 1 |
Person.isArrived() | 1 | 1 | 1 |
Person.isInElevator() | 1 | 1 | 1 |
Person.isSelectA() | 1 | 1 | 1 |
Person.isSelectB() | 1 | 1 | 1 |
Person.isSelectC() | 1 | 1 | 1 |
Person.setChanged() | 1 | 1 | 1 |
Person.setCurrentFloor(int) | 1 | 1 | 1 |
Person.setCurrentTarget(Elevator) | 3 | 3 | 6 |
Person.setCurrentTargetA() | 1 | 16 | 16 |
Person.setCurrentTargetB() | 1 | 16 | 16 |
Person.setCurrentTargetC() | 1 | 16 | 16 |
Person.setIn() | 1 | 1 | 1 |
Scheduler.CreateElevator(String,String) | 1 | 1 | 1 |
Scheduler.Scheduler() | 1 | 1 | 1 |
Scheduler.StartElevators() | 1 | 2 | 2 |
Scheduler.SubLeft() | 1 | 1 | 1 |
Scheduler.getInstance() | 1 | 1 | 2 |
Scheduler.getThreadpool() | 1 | 1 | 1 |
Scheduler.isRequestLeft() | 2 | 1 | 2 |
Scheduler.putRequest(Person,boolean) | 1 | 6 | 7 |
Scheduler.takeRequest(Elevator) | 4 | 9 | 14 |
Total | 85 | 161 | 185 |
Average | 1.37 | 2.60 | 2.98 |
比较明显的复杂度较高的方法是setCurrentTargetX()
,因为不仅要判断接下来能乘坐的电梯,还要从能换乘的楼层中选取一个较好的换乘楼层,此处进行了大量的条件判断和循环。
Class | OCavg | WMC |
---|---|---|
Client | 3 | 6 |
Elevator | 2.05 | 45 |
ElevatorA | 1.33 | 4 |
ElevatorB | 1.33 | 4 |
ElevatorC | 1.33 | 4 |
ElevatorFactory | 5 | 5 |
MainClass | 1 | 1 |
Person | 3.61 | 65 |
Scheduler | 3.11 | 28 |
(4) 协作图
(5) 可扩展性
本次作业主要注重功能上的完整性,对于性能的优化并未做进一步扩展,在性能上仍有很大扩展空间,比如依据电梯容量和电梯速率的不同调度策略,在调度策略上还有很多东西可以做。考虑到未来可能增加的暂停某部电梯的请求,这种通过threadpool的架构方式也能很轻易地对电梯进行暂停控制。
(6) SOLID
对于SPR原则,我并没有专门设置接口来管理输出,就这一点而言做得可能还不是很好。在OCP原则上,我认为我的程序可扩展性还有一定问题,如果出现新的电梯型号,Person很多关于换乘策略的判断方法都需要进行相应添加和修改。其它原则上我认为做得还不错。
Bug分析
本单元中,前两次作业均未测出bug,在课下遇到的最主要困难就是线程安全方面的问题。但是第三次作业出了大问题,整个测试爆炸,在做第二次作业的时候我就在思考第三次作业可能的架构,但是在编写完成后虽然功能上没有了问题,评测机却频繁爆出RE和CTLE,猜想某处发生了轮询,但是始终未能发现真正的问题所在,最后只提交了不完成品,果然测试爆炸。经过几天思索,仔细检查代码中的逻辑,运用jprofiler对进程进行监控,最终发现了问题。我在第三次最初的架构上没有采用将请求分成A, B, C三类的策略,而是让所有电梯*竞争,若没有适合的请求则wait并且notifyAll,导致的结果就是一些电梯进程不断进入Scheduler试图获取request,但每次都拿不到,从profiler中可看到,一个线程可能在Scheduler中空转数秒后,正确的电梯线程才能获取到请求。这种不加分类完全随机的策略导致电梯线程长时间“扑空”,将请求分类,电梯扑空后等待,只有对应请求队列再次出现请求时才被唤醒。更改架构后算是较好地解决了问题。
此次出现此bug最重要的原因就是最初构思时完全陷入了错误道路,前两次作业电梯没有停靠楼层的限制,自然不会出现这种奇妙的“连续扑空”现象。
Hack策略
此次hack用到了自己写的评测机器,随机构筑相应测试集后通过黑盒测试来进行hack。
心得体会
首次接触多线程编程,不得不说,受益良多。其中体会最深的有三点。一是多线程编程中最需要注意的事情:线程安全!!!很多奇奇怪怪的bug都出现在线程安全上,而且由于多线程的随机性,这些bug可能很难复现,如果最开始不考虑清楚后面会非常痛苦。二是要学会在性能和安全之间寻找平衡,不能一味追求安全忽略性能,当然更不能因为性能忽略安全,要在安全的前提下尽力提升性能。三是多线程debug的难度是真的高,不管是debuger的加入还是print大法都有可能改变线程的调度情况,使得正常运行的程序和debuger观察中的程序变得不一样,很多时候可能需要借助jprofiler这种工具来对线程直接进行监控,并且多次尝试才能最终发现bug。