OO第二单元总结

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类图
OO第二单元总结

​ 如图所示,是一个非常简单的结构,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-elsefor的嵌套,复杂度较高

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) 协作图

OO第二单元总结


第二次作业

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类图

OO第二单元总结

​ 如图所示,由于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) 协作图

OO第二单元总结


第三次作业

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类图

OO第二单元总结

​ 可以看到,Client通过调用Scheduler中的方法来创建电梯线程,电梯对象的创建则由一个简单的工厂来完成。Person类中多了一些属性,isSelectX提供给Scheduler,表示自己能乘坐哪些种类的电梯,还有多了一个中间的currentFloorcurrentTarget,用来表示当前所在楼层,和当前目标,方便电梯进行捎带,运输。

(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) 协作图

OO第二单元总结

(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。

上一篇:SpringBoot+Quartz+MySQL实现分布式定时任务


下一篇:Python APscheduler