OO第二单元总结
前言
在第二单元,我和闻名已久的多线程有了正面交锋,感受到了一定的难度。我在第二单元投入了更多的时间,也收获了更多知识和经验,以此博客总结。
第一次作业
程序结构分析
UML类图
程序讲解
本单元第一次作业是自行设计调度策略的单电梯运输乘客。首先,我根据课程学习的生产者——消费者模型确定了大体框架:输入为一个线程(生产者),电梯为一个线程(消费者),两个线程通过共享Storage(托盘)来完成乘客运输。最开始采用指导书的ALS调度策略,稍后经过查阅资料,发现Look更适合本次作业,于是另外加了Look调度算法,并在运行方向的判断上进行了一定的改进,最后完成了程序。
时序图
耦合度分析
class | OCavg | WMC |
---|---|---|
AlsElevator | 6.4 | 32.0 |
Input | 2.0 | 4.0 |
LookElevator | 7.6 | 38.0 |
LookInput | 2.0 | 4.0 |
LookStorage | 1.5 | 12.0 |
MainClass | 1.0 | 1.0 |
Storage | 1.89 | 17.0 |
Total | 108.0 | |
Average | 3.38 | 15.43 |
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
AlsElevator.AlsElevator(Storage) | 1.0 | 1.0 | 1.0 |
AlsElevator.dealRequest() | 7.0 | 12.0 | 15.0 |
AlsElevator.judge() | 1.0 | 7.0 | 9.0 |
AlsElevator.move() | 1.0 | 2.0 | 4.0 |
AlsElevator.run() | 3.0 | 8.0 | 8.0 |
Input.Input(Storage) | 1.0 | 1.0 | 1.0 |
Input.run() | 3.0 | 4.0 | 4.0 |
LookElevator.dealRequest() | 7.0 | 12.0 | 15.0 |
LookElevator.judge() | 5.0 | 7.0 | 14.0 |
LookElevator.LookElevator(LookStorage) | 1.0 | 1.0 | 1.0 |
LookElevator.move() | 1.0 | 2.0 | 4.0 |
LookElevator.run() | 3.0 | 8.0 | 8.0 |
LookInput.LookInput(LookStorage) | 1.0 | 1.0 | 1.0 |
LookInput.run() | 3.0 | 4.0 | 4.0 |
LookStorage.add(PersonRequest) | 1.0 | 1.0 | 1.0 |
LookStorage.dealRequest(PersonRequest) | 1.0 | 1.0 | 1.0 |
LookStorage.dispatcher() | 3.0 | 1.0 | 3.0 |
LookStorage.endExit() | 1.0 | 1.0 | 1.0 |
LookStorage.getAcceptableRequests(int) | 1.0 | 3.0 | 3.0 |
LookStorage.getQueue() | 1.0 | 1.0 | 1.0 |
LookStorage.LookStorage() | 1.0 | 1.0 | 1.0 |
LookStorage.update() | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 1.0 | 1.0 | 1.0 |
Storage.add(PersonRequest) | 1.0 | 1.0 | 1.0 |
Storage.dealRequest(PersonRequest) | 1.0 | 1.0 | 1.0 |
Storage.dispatcher() | 3.0 | 1.0 | 3.0 |
Storage.endExit() | 1.0 | 1.0 | 1.0 |
Storage.getAcceptableRequests(int) | 1.0 | 4.0 | 4.0 |
Storage.getMain() | 3.0 | 1.0 | 3.0 |
Storage.judgeSameDirection(PersonRequest,PersonRequest) | 2.0 | 2.0 | 2.0 |
Storage.Storage() | 1.0 | 1.0 | 1.0 |
Storage.update() | 1.0 | 1.0 | 1.0 |
Total | 63.0 | 94.0 | 119.0 |
Average | 1.97 | 2.94 | 3.72 |
文字分析
第一次写多线程还是进行了很多的尝试的。本身写的是ALS调度,但是后来感觉性能不够理想改成了Look调度,其实这两个调度完全可以单独成类,而我却把调度直接在电梯里书写方法,导致电梯类的代码量较大,耦合度较高,这也是我程序的不足。电梯的运行和工作采取了状态机的方法,可以比较清晰简单的结束程序,不会出现程序无法结束的bug。
bug分析
本人:本次作业在书写之前进行了一天的构思和设计,设计的比较细致,加上本次作业比较简单,代码是一遍写成 + 一遍优化,没有出现bug。
他人:在互测中发现以下问题:
1.调度无脑,有超时嫌疑,但因互测条件放宽,不会出现超时,但是会大程度影响性能。
2.在面对方向不同的请求的时候出现bug,电梯反复在两层楼中运行,导致程序无法结束,应该是电梯内部调度设计不够仔细。
第二次作业
结构分析
UML类图
程序讲解
本次作业是多部电梯运行,同时加入了最大人数限制,主要考察了更多线程的交互与同步。
本次作业的框架是在主线程中输入,将请求储存在公共区(Dispatcher中的阻塞队列),同时根据电梯数目启动相应数目的线程。调度器(Dispatcher)根据每个电梯的状态(采用最近请求优先与负载均衡原则)分配请求到电梯内部的阻塞队列。之后电梯内部的调度和运行延续第一次作业。
时序图
一层调度(外度调度)
二层调度(内部调度)
耦合度分析
class | OCavg | WMC |
---|---|---|
Dispatcher | 2.43 | 34.0 |
Elevator | 3.92 | 47.0 |
MainClass | 4.0 | 4.0 |
MainRunStatus | 1.0 | 3.0 |
Struct | 1.83 | 11.0 |
Total | 99.0 | |
Average | 2.75 | 19.8 |
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Dispatcher.addRequests(PersonRequest) | 1.0 | 1.0 | 1.0 |
Dispatcher.dealRequest(PersonRequest) | 1.0 | 1.0 | 1.0 |
Dispatcher.dispatch() | 4.0 | 5.0 | 5.0 |
Dispatcher.dispatch1() | 4.0 | 5.0 | 5.0 |
Dispatcher.dispatch2() | 4.0 | 5.0 | 5.0 |
Dispatcher.Dispatcher(int,MainRunStatus) | 1.0 | 2.0 | 2.0 |
Dispatcher.endExit() | 1.0 | 1.0 | 1.0 |
Dispatcher.getDifference(int,int) | 2.0 | 1.0 | 2.0 |
Dispatcher.getElevators() | 1.0 | 1.0 | 1.0 |
Dispatcher.getFinishedDeal() | 1.0 | 1.0 | 1.0 |
Dispatcher.getMin(ArrayList) | 1.0 | 1.0 | 3.0 |
Dispatcher.getMinEle() | 1.0 | 2.0 | 3.0 |
Dispatcher.sortedEle(int) | 1.0 | 3.0 | 3.0 |
Dispatcher.update() | 1.0 | 1.0 | 1.0 |
Elevator.addRequest(PersonRequest) | 1.0 | 1.0 | 1.0 |
Elevator.deal() | 1.0 | 4.0 | 4.0 |
Elevator.dealRequest() | 3.0 | 7.0 | 8.0 |
Elevator.Elevator(int,Dispatcher) | 1.0 | 1.0 | 1.0 |
Elevator.getAcceptableRequests() | 1.0 | 3.0 | 3.0 |
Elevator.getName() | 1.0 | 1.0 | 1.0 |
Elevator.getNowFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getPersonNum() | 1.0 | 1.0 | 1.0 |
Elevator.getRequestNumber() | 1.0 | 1.0 | 1.0 |
Elevator.judge() | 7.0 | 11.0 | 22.0 |
Elevator.move() | 1.0 | 1.0 | 6.0 |
Elevator.run() | 3.0 | 4.0 | 5.0 |
MainClass.main(String[]) | 3.0 | 4.0 | 4.0 |
MainRunStatus.getGoOn() | 1.0 | 1.0 | 1.0 |
MainRunStatus.MainStart() | 1.0 | 1.0 | 1.0 |
MainRunStatus.MainStop() | 1.0 | 1.0 | 1.0 |
Struct.compareTo(Object) | 5.0 | 4.0 | 5.0 |
Struct.getDiff() | 1.0 | 1.0 | 1.0 |
Struct.getDifference(int,int) | 2.0 | 1.0 | 2.0 |
Struct.getElevatorRequestNumber() | 1.0 | 1.0 | 1.0 |
Struct.getId() | 1.0 | 1.0 | 1.0 |
Struct.Struct(Elevator,int,int) | 1.0 | 1.0 | 1.0 |
Total | 63.0 | 82.0 | 106.0 |
Average | 1.75 | 2.28 | 2.94 |
文字分析
本次作业极易出现死锁问题,我在避免死锁的设计上投入了较多的时间,加上调度策略综合了电梯的状态进行了尽可能的优化,个人感觉程序性能和正确性还是可以的。设计上相比上次进行了一定程度的解耦,比如电梯状态的单独成类可以一定程度上降低耦合度,同时可以方便调度器获取电梯信息,从而进行请求排序和调度,个人感觉struct的设计比较适当。但是,由于电梯相比上次有了新的限制,因此电梯的代码量有所增加,加上调度器功能的增加(本人设计了3种调度模式),导致调度器的代码量较大。
SOLID原则(由于本次作业不涉及继承和自己写的接口等,因此对于里氏替换原则、接口分离原则以及依赖倒置原则不进行分析):
1.单一责任原则:
对于电梯的状态的获取,采用单独的包装类Struct类,从而减轻了电梯的责任负担,也使电梯的功能比较单一,仅负责运输乘客。
2.开放封闭原则:
dispatcher类中设计了三种调度方法,在一定程度上可以满足不同需求的电梯调度,具有一定的扩展性。
bug分析
个人分析:本次作业由于调度器在调度时需要获得电梯的信息,电梯运行时又需要等待调度器的调度,因此有可能会出现相互等待的死锁问题。因此在设计上需格外小心,具体方法在下面的死锁解决部分讨论,程序没有出现死锁bug。但是,由于本次作业电梯的运行有了人数限制,在分派请求之后电梯内部调度本身由于设计不周,导致运行时出现不应该的错误,可见个人书写代码还是不够缜密。好在课下发现了bug,在强测和互测中均没有出现其他的bug,实属幸运。
别人bug分析:因为知道本次作业容易出现死锁,故构造个请求间隔时间段较大的数据,使得死锁出现的概率增大,找到了别人的死锁bug并成功hack。
第三次作业
结构分析
UML类图
程序讲解
本次作业加入了电梯增加的请求,整体采用上次的分配策略。但是由于电梯的楼层有了一定的限制,因此需要进行请求分割。由于课程组提供的请求输入为jar包形式,因此我们在分割请求之后无法将请求直接加入请求队列,因此构造新的类Person类,表示乘客请求,同时加入暂时停留楼层的属性(tempFloor),具有两种生成方法:1.根据课程组提供的PersonRequest解析id,toFlower,fromFloor,从而生成;2.根据输入的整数生成。这样就方便我们进行请求分割的操作了。
本次作业,我采用的是静态的请求分割,即请求一但输入,在生成Person的时候对请求进行分割,请求分为:A类,B类,C类,AB类,BC类,ABC类(对应各种电梯)表示该请求最开始会被分配的电梯类型,同时解析请求换层的楼层(tempFloor),如果不需要换乘,那么tempFloor为0。之后调度根据请求对应的类型进行局部电梯分配,之后进行电梯内部的调度,这最后的两步和第二次作业相似。
时序图
一层调度(外部调度)
二层调度(内部调度)
耦合度分析
class | OCavg | WMC |
---|---|---|
Dispatcher | 2.82 | 48.0 |
Elevator | 4.071428571428571 | 57.0 |
MainClass | 6.0 | 6.0 |
Person | 3.5 | 56.0 |
PrintHelper | (空类,为输出同步设计) | 0.0 |
Struct | 2.43 | 17.0 |
Total | 184.0 | |
Average | 3.35 | 30.67 |
method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Dispatcher.add(Person) | 1.0 | 1.0 | 1.0 |
Dispatcher.addElevator(Elevator) | 1.0 | 3.0 | 3.0 |
Dispatcher.dealRequest() | 1.0 | 1.0 | 1.0 |
Dispatcher.dispatch() | 1.0 | 8.0 | 8.0 |
Dispatcher.dispatchA(int,Person) | 3.0 | 4.0 | 4.0 |
Dispatcher.dispatchAB(int,Person) | 3.0 | 4.0 | 4.0 |
Dispatcher.dispatchABc(int,Person) | 3.0 | 4.0 | 4.0 |
Dispatcher.dispatchB(int,Person) | 3.0 | 4.0 | 4.0 |
Dispatcher.dispatchBC(int,Person) | 3.0 | 4.0 | 4.0 |
Dispatcher.dispatchC(int,Person) | 3.0 | 4.0 | 4.0 |
Dispatcher.Dispatcher(PrintHelper) | 1.0 | 1.0 | 1.0 |
Dispatcher.getElevators() | 1.0 | 1.0 | 1.0 |
Dispatcher.getFinishedDeal() | 1.0 | 1.0 | 1.0 |
Dispatcher.getMinEle(ArrayList) | 1.0 | 2.0 | 3.0 |
Dispatcher.setFinishedInput() | 1.0 | 1.0 | 1.0 |
Dispatcher.sortedEle(int,ArrayList) | 1.0 | 3.0 | 3.0 |
Dispatcher.update() | 1.0 | 1.0 | 2.0 |
Elevator.addRequest(Person) | 1.0 | 1.0 | 1.0 |
Elevator.deal() | 1.0 | 4.0 | 4.0 |
Elevator.dealRequest() | 3.0 | 8.0 | 9.0 |
Elevator.Elevator(String,Dispatcher,String,PrintHelper) | 1.0 | 4.0 | 4.0 |
Elevator.getAcceptableRequests() | 1.0 | 3.0 | 3.0 |
Elevator.getName() | 1.0 | 1.0 | 1.0 |
Elevator.getNowFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getPersonNum() | 1.0 | 1.0 | 1.0 |
Elevator.getRequestNumber() | 1.0 | 1.0 | 1.0 |
Elevator.getType() | 1.0 | 1.0 | 1.0 |
Elevator.judge() | 8.0 | 16.0 | 23.0 |
Elevator.move() | 1.0 | 1.0 | 6.0 |
Elevator.run() | 3.0 | 4.0 | 5.0 |
Elevator.setStatus(int,int) | 1.0 | 1.0 | 5.0 |
MainClass.main(String[]) | 3.0 | 6.0 | 6.0 |
Person.contains(int[],int) | 3.0 | 1.0 | 3.0 |
Person.getFromFloor() | 1.0 | 1.0 | 1.0 |
Person.getPersonId() | 1.0 | 1.0 | 1.0 |
Person.getTempToFloor() | 1.0 | 1.0 | 1.0 |
Person.getToFloor() | 1.0 | 1.0 | 1.0 |
Person.getType() | 1.0 | 1.0 | 1.0 |
Person.judgeType() | 1.0 | 7.0 | 7.0 |
Person.Person(int,int,int) | 1.0 | 1.0 | 1.0 |
Person.Person(PersonRequest) | 1.0 | 1.0 | 1.0 |
Person.setA() | 1.0 | 1.0 | 4.0 |
Person.setAB() | 5.0 | 2.0 | 5.0 |
Person.setABc() | 6.0 | 2.0 | 6.0 |
Person.setB() | 1.0 | 1.0 | 6.0 |
Person.setBC() | 5.0 | 2.0 | 7.0 |
Person.setC() | 1.0 | 1.0 | 4.0 |
Person.setTempToFloor() | 1.0 | 10.0 | 10.0 |
Struct.compareTo(Object) | 7.0 | 6.0 | 7.0 |
Struct.getDiff() | 1.0 | 1.0 | 1.0 |
Struct.getDifference(int,int) | 2.0 | 1.0 | 2.0 |
Struct.getElevatorRequestNumber() | 1.0 | 1.0 | 1.0 |
Struct.getElevatorType() | 1.0 | 1.0 | 1.0 |
Struct.getId() | 1.0 | 1.0 | 1.0 |
Struct.Struct(Elevator,int,int) | 1.0 | 4.0 | 4.0 |
Total | 102.0 | 149.0 | 196.0 |
Average | 1.85 | 2.71 | 3.56 |
文字分析
本次作业的设计亮点在于请求分割,这个分割需要和自己的调度策略结合,才能发挥较好地效果。因为我的调度策略3采用的是优化的look调度,能够一定程度上配合我的请求分割策略,故性能达到了较好的水平。请求分割在Person内部实现,通过tempFloor的设定可以达到换乘的效果。
本次作业的最大设计思想就是:简化电梯功能,因为本身电梯的运行以及状态判断已经较为复杂,如果每个请求的换乘设计还在电梯内部实现,那么电梯类的方法过多,会导致耦合度较高。因此增加了Person类的一些分割操作,电梯只需根据Person的tempFloor是否为0判断乘客的目的楼层应该根据tempFloor停靠还是toFloor停靠,因为dispatcher在请求分配的时候,不会将请求分配到不正确的电梯中。
SOLID原则(由于本次作业不涉及继承和自己写的接口等,因此对于里氏替换原则、接口分离原则以及依赖倒置原则不进行分析):
1.单一责任原则:
对于电梯的状态的获取,采用单独的包装类Struct类和Person类(在上述分析中体现),从而减轻了电梯的责任负担,也使电梯的功能比较单一,仅负责运输乘客。
2.开放封闭原则:
由于dispatcher中已经根据不同的电梯类型设计了不同的分配请求方法,因此面对不同的调度需求有较好的扩展性,在后部分的可扩展性分析也可以体现。
bug分析
个人分析:本次由于简化电梯功能的设计思想,对于请求分配有较高的要求,我在设计请求分割和请求分配(外部调度)上花费了较多的时间,加上整体的框架延续了第二次作业,故最后的代码正确性有一定的保证,顺利完成测试。
别人bug分析:本次互测分配的朋友,有一位在指令较多的时候有Rtle现象,但是由于评测机指令时间限制,改造数据上交后无法复现,也是我三次作业hack时间最长的一次,出现了0/63的尴尬场面....
第三次作业可扩展性分析
第三次作业的设计出发点是保证正确性,性能达到自己力所能及的最好即可。其实多线程的难点在于实现各线程的状态信息的交互,个人认为本程序在这方面的设计可以达到安全的传递信息的目的。接下来,针对第三次作业的功能和性能难点分析程序。
首先,是信息交互的死锁问题。在多个电梯线程中,仅仅对于方法加上synchronized的字样是远远不够的,而我们清楚电梯之间的信息交互是通过dispatcher的dispatch方法实现的,那么我们就产生了同步的设计思路:在调用dispatch的地方通过synchronized(dispatcher)实现,因为所有电梯的dispatcher是共享的,所以这样可以保证电梯调度的有序性并且避免死锁。
其次,是调度性能的追求使bug出现概率的增大。这个是不可避免的定律,我们能做的只能是设计上的认真细致,层层保证正确性从而达到最终程序的正确性保证。在保证信息交互安全之后,就是保证请求分割的正确,详细分析自己的分割策略,并对分割请求这一部分(单线程)进行全覆盖测试,在之后就是调度请求,根据请求分割时进行的类型判断,选用不同的电梯集合进行分配,这样可以最大程度上减少错误的出现。
最后,是程序的扩展性。虽然这是最后一次作业,说实话,我在设计的时候是没有考虑扩展性的,但是总结的时候还是可以思考一下的。
1.面对可能出现的电梯故障问题,我们可以直接在电梯内部增加状态,在这个状态将乘客在当前楼层停下,遍历电梯队列里的请求,生成新的请求加入总队列即可。该部分对于本程序较为简单。
2.面对乘客优先级,由于我的乘客是新的类别(Person类)因此,直接增加属性,并在Person类里面重写comparaTo方法,从而在请求队列里面进行排序,调度的时候根据该顺序调度即可。但该部分需要修改原本的电梯内部调度(这个不可避免会造成一定程度的性能损失)。
3.面对一系列的重量等限制,其实整体和人数限制思路相似,只是更改电梯内的一两个属性并且判断乘客能否上电梯的条件进行修改即可。
本次作业还是有很大的扩展空间的,祝来年的学弟学妹好运。
心得与体会
多线程作业对我们的设计提出了新的要求,我们在写程序之前就要思考可能出现的“线程危险”,从而在写程序的时候尽量避免。同时,加上性能的要求,给人“有形”的压力。我愈发体会到设计的重要性。这几次作业我基本在周二晚上不敲代码,进行经过认真的设计,从周三下午开始实现的。这样做减轻了我后期找bug的负担,也给优化带来了便捷。我们都知道多线程bug的复现比较玄学,对于著名的死锁问题有时也是很难复现。但是我们清楚死锁的闭环条件,在设计的时候避免闭环的形成,或许比先设计出一版有效作业再进行死锁纠察效果要好很多。同时,和同学们讨论也是很好的性能优化方法。这三次作业有很大的自由性,应该没有完全一样的调度方法,在和别人交流调度策略的时候往往可以有所收获,优化自己的调度策略。
总之,这三周与多线程相识,确实学到了很多,也达到了自己的小目标。希望之后的OO作业也能像这三周一样认真高效地完成。