OO第二单元总结
OO第二单元总结博客
第五次作业
(1)设计策略
第五次作业的目标是在五栋楼内分别实现一部可纵向运输乘客的电梯,主要考察关于线程的创建、运行等基本操作以及可能会出现的多线程安全问题。考虑到之后作业可能会进行的拓展,在本次作业中我除了实现输入、电梯线程之外,还加入了调度器线程,但调度器线程的实现比较简单,将在下文进行介绍。在调度策略方面针对ALS算法进行了一定程度的优化,线程安全方面主要采用同步块和wait
、notify
协调工作。
第五次作业以及第二单元之后的作业采用的都是生产者-消费者模式,主要区别在于中间介质以及生产者、消费者数量的不同。
(2)同步块的设置和锁的选择
第五次作业的整体架构比较简单,在我的设计中,输入线程和调度器线程之间通过总请求队列waitQueue
进行进程间的信息交流,输入线程读取到乘客请求后将请求加入到waitQueue
,调度器从waitQueue
中不断取出请求。在这里,我将waitQueue
封装成了一个类,在类中定义了对waitQueue
的查询,加入、删除等操作,在这些函数前都加入了synchronized
关键字,保证每次只有一个线程访问主请求队列,保证了线程安全。
除了保证线程安全之外,为了防止不必要的轮询,设计中使用了wait
和notify
进行协同工作,当调度器试图从主请求队列中取出请求但请求队列为空时,会执行wait()
操作进入休眠,让出CPU的控制权,当输入线程向请求队列加入新的请求时会调用notifyAll()
将调度器线程唤醒。
电梯线程与调度器线程之间通过各个楼的请求队列processingQueue
进行线程间的通信,processingQueue
和waitQueue
同属一个类,因此关于processingQueue
线程安全问题的解决方式与上文介绍的相同,都是采用同步块与wait
、notify
相结合的方式。
在电梯线程内部加入了捎带队列subQ
和电梯内乘客队列insideQ
,和processingQueue
、waitQueue
同属一个类,因此相关操作都是线程安全的。
(3)调度器设计
在第五次作业中,调度器的功能比较简单,与输入线程通过waitQueue
总请求队列进行信息交互,与每栋楼的电梯通过processingQueue
进行信息交互。调度器不断从输入线程取出请求并根据请求所在楼座将请求分配给相应的电梯,逻辑比较简单。
(4)程序结构
UML类图
可见本次作业的架构非常简单,所使用的类也很少,ALS调度策略的实现都被封装在了电梯內部,其它类的功能就是不断将请求传递给目标电梯。
复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
hwone.Elevator.Elevator(Integer, InputReq, Schedule, char, RequestQueue) | 0 | 1 | 1 | 1 |
hwone.Elevator.canTake(PersonRequest) | 16 | 1 | 11 | 26 |
hwone.Elevator.decideOpen() | 14 | 8 | 8 | 12 |
hwone.Elevator.downOneFloor() | 14 | 1 | 9 | 9 |
hwone.Elevator.getDirection() | 3 | 1 | 1 | 3 |
hwone.Elevator.printReq() | 1 | 1 | 2 | 2 |
hwone.Elevator.run() | 35 | 4 | 13 | 15 |
hwone.Elevator.setTarFloor() | 2 | 1 | 2 | 2 |
hwone.Elevator.stopOneFloor() | 14 | 1 | 9 | 9 |
hwone.Elevator.upOneFloor() | 14 | 1 | 9 | 9 |
hwone.Elevator.updataMainTaskByInside() | 0 | 1 | 1 | 1 |
hwone.Elevator.updateSubQ() | 4 | 1 | 4 | 4 |
hwone.InputReq.InputReq(RequestQueue) | 0 | 1 | 1 | 1 |
hwone.InputReq.addObs(Schedule) | 0 | 1 | 1 | 1 |
hwone.InputReq.run() | 4 | 3 | 3 | 3 |
hwone.MainClass.main(String[]) | 0 | 1 | 1 | 1 |
hwone.RequestQueue.RequestQueue() | 0 | 1 | 1 | 1 |
hwone.RequestQueue.addRequest(PersonRequest) | 0 | 1 | 1 | 1 |
hwone.RequestQueue.getHead() | 5 | 2 | 4 | 5 |
hwone.RequestQueue.getOneRequest() | 5 | 2 | 4 | 5 |
hwone.RequestQueue.getRequests() | 0 | 1 | 1 | 1 |
hwone.RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
hwone.RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
hwone.RequestQueue.pop() | 0 | 1 | 1 | 1 |
hwone.RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
hwone.Schedule.Schedule(RequestQueue, InputReq) | 0 | 1 | 1 | 1 |
hwone.Schedule.addObs(RequestQueue) | 0 | 1 | 1 | 1 |
hwone.Schedule.forward(PersonRequest) | 5 | 1 | 5 | 5 |
hwone.Schedule.run() | 9 | 4 | 5 | 6 |
Class | OCavg | OCmax | WMC | |
hwone.Elevator | 4.42 | 9 | 53 | |
hwone.InputReq | 1.67 | 3 | 5 | |
hwone.MainClass | 1 | 1 | 1 | |
hwone.RequestQueue | 1.44 | 3 | 13 | |
hwone.Schedule | 3 | 5 | 12 |
可见电梯类Elevator
是设计中最为复杂和臃肿的一个类,因为本次作业使用的ALS调度策略都被封装在了这个类中,在电梯线程run
的过程中不断调用相关函数,这就导致复杂度都集中在了某个类和某个类的方法中。在本次作业中我也没重视这个问题,导致在第6次作业中我不得不对电梯类进行拆解,耗费了很多不必要的开发时间。
(5)Bug分析
本次作业架构的设计思路来自于课上训练的生产者-消费者模型,特别是对于队列类waitQueue
的实现基本上照搬了课上的训练代码,在课上的训练代码中,这个类中的大部分函数的结尾都有notifyAll()
去唤醒阻塞线程,当时并没有太在意,而且在本地测试和中测阶段都没有出现问题,但是到了强测出现了很多CPU超时的错误。通过对整个程序线程通信关系的重新梳理,发现正是waitQueue
中很多无谓的notifyAll
导致本该休眠的线程被不断唤醒,不断占用CPU的资源从而导致了超时。
强测阶段
强测阶段出现的Bug都是由于线程被无故唤醒导致的,修复策略就是不要无脑加锁和notify
互测阶段
互测阶段没有被hack出bug,阅读了同房间其他同学的代码也没有发现什么bug,自己也没有去刻意构造强测数据去hack
第六次作业
(1)设计策略
第六次作业在第五次作业的基础上加入了横向传送请求,新增了楼座与楼座之间移动的环形电梯,由于没有转运请求,因此可以看作第五次作业纵向电梯的变种,不同点在于需要调整一下电梯的调度算法。本次作业的架构继承自第五次作业,仅仅在少数地方进行了改动,在电梯中增加了type
属性用于区分横向电梯和纵向电梯,同时设置两种不同的工作模式。纵向电梯依然采用改进版的ALS调度策略,考虑到总共仅有5栋楼座,因此选择让横向电梯仅朝一个方向运动,这样做的好处在于实现简单且不易出现错误,性能损失也在一个可以接受的范围内。
(2)同步块的设置和锁的选择
本次作业相比第五次作业仅有少量修改,在线程安全方面依然使用同步块和wait
、notify
协调工作,线程之间通过各个请求队列进行信息交互,所有对请求队列的操作都被synchronize
修饰。值得注意的一点是,尽管在第五次作业中就有部分同学发现了输出线程的不安全问题,但我并没有重视,在这次作业中果然出现了由于输出线程不安全导致的bug,因此我将输出函数封装为一个类,在类中实现了线程安全的输出函数。
(3)调度器设计
在第六次作业中,调度器的实现采用了自由竞争的策略,针对五栋楼座和十层楼分别创建五个纵向请求队列和十个横向请求队列,相应楼座和楼层的电梯各凭本事()去争夺请求。这样做的好处在于调度策略是动态的,每个电梯的工作量比较平均,不易出现阻塞现象。调度器的工作逻辑和第五次作业大致相同,都是不断从输入线程中读取请求然后根据请求的种类将其加入到相应楼座或楼层的请求队列,之后电梯自由竞争获得请求。
(4)程序结构
UML图
由于本次作业加入了横向电梯,所以在Elevator
类中加入了横向运输的相关操作,同时在Schedule
中将横向请求队列和纵横请求队列封装到了容器内
复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
hwsix.Elevator.Elevator(int, InputReq, Schedule, Integer, RequestQueue, Integer) | 0 | 1 | 1 | 1 |
hwsix.Elevator.Elevator(int, InputReq, Schedule, char, RequestQueue, Integer) | 0 | 1 | 1 | 1 |
hwsix.Elevator.canTake(PersonRequest) | 23 | 3 | 12 | 29 |
hwsix.Elevator.decideOpen() | 44 | 16 | 16 | 24 |
hwsix.Elevator.downOneFloor() | 14 | 1 | 9 | 9 |
hwsix.Elevator.getDirection() | 3 | 1 | 1 | 3 |
hwsix.Elevator.getRollState() | 2 | 1 | 1 | 2 |
hwsix.Elevator.run() | 4 | 1 | 3 | 3 |
hwsix.Elevator.setTarBuilding() | 2 | 1 | 2 | 2 |
hwsix.Elevator.setTarFloor() | 2 | 1 | 2 | 2 |
hwsix.Elevator.stopOneBuilding() | 14 | 1 | 9 | 9 |
hwsix.Elevator.stopOneFloor() | 14 | 1 | 9 | 9 |
hwsix.Elevator.turnOneBuilding() | 19 | 1 | 9 | 13 |
hwsix.Elevator.upOneFloor() | 14 | 1 | 9 | 9 |
hwsix.Elevator.updataMainTaskByInside() | 0 | 1 | 1 | 1 |
hwsix.Elevator.updateSubQ() | 4 | 1 | 4 | 4 |
hwsix.Elevator.workModelOne() | 35 | 4 | 13 | 15 |
hwsix.Elevator.workModelTwo() | 17 | 4 | 7 | 9 |
hwsix.InputReq.InputReq(RequestQueue, ArrayList |
0 | 1 | 1 | 1 |
hwsix.InputReq.addObs(Schedule) | 0 | 1 | 1 | 1 |
hwsix.InputReq.run() | 22 | 3 | 10 | 10 |
hwsix.MainClass.main(String[]) | 2 | 1 | 3 | 3 |
hwsix.Printer.println(String) | 0 | 1 | 1 | 1 |
hwsix.RequestQueue.RequestQueue() | 0 | 1 | 1 | 1 |
hwsix.RequestQueue.addRequest(PersonRequest) | 0 | 1 | 1 | 1 |
hwsix.RequestQueue.getHead() | 5 | 2 | 4 | 5 |
hwsix.RequestQueue.getOneRequest() | 5 | 2 | 4 | 5 |
hwsix.RequestQueue.getRequests() | 0 | 1 | 1 | 1 |
hwsix.RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
hwsix.RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
hwsix.RequestQueue.pop() | 0 | 1 | 1 | 1 |
hwsix.RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
hwsix.Schedule.Schedule(RequestQueue, InputReq, ArrayList |
0 | 1 | 1 | 1 |
hwsix.Schedule.forwardBuilding(PersonRequest) | 5 | 1 | 5 | 5 |
hwsix.Schedule.forwardFloor(PersonRequest) | 0 | 1 | 1 | 1 |
hwsix.Schedule.run() | 15 | 4 | 7 | 8 |
Class | OCavg | OCmax | WMC | |
hwsix.Elevator | 5.17 | 16 | 93 | |
hwsix.InputReq | 4 | 10 | 12 | |
hwsix.MainClass | 3 | 3 | 3 | |
hwsix.Printer | 1 | 1 | 1 | |
hwsix.RequestQueue | 1.44 | 3 | 13 | |
hwsix.Schedule | 3.5 | 7 | 14 |
可以看出本次作业的类复杂度主要都集中在Elevator
、InputReq
和Schedule
,而由于横向电梯的加入,Elevator
类的复杂度进一步提高,我也没有进行很好的简化合并,导致本次作业整体架构不是特别理想
(5)Bug分析
本次作业出现的Bug仍然与线程安全有关,程序中依然存在一些无谓的notify
导致线程被不断唤醒,占用了CPU的资源导致了超时。另外在强测之前及时发现了输出线程的安全问题。
第七次作业
(1)设计策略
第七次作业在第六次作业的基础上加入了换乘请求,即请求的起始楼座和目的楼座可以不同,因此加强了线程之间的信息交互要求,除此之外还加入了电梯的定制化需求。整体架构相比第六次作业并没有很大的改动,由于在第六次作业中Elevator
已经非常臃肿,因此在这次作业当中我将Elevator
类拆解为FloorElevator
和BuildingElevator
,即横向电梯和纵向电梯。另外由于换乘请求的加入,乘客的目的楼座和楼层会动态改变,因此我将原先的PersonRequest
类重新封装为Person
类,相应的请求队列属性也发生改变,最后更改了调度器的实现逻辑。
(2)同步块的设置和锁的选择
本次作业线程安全问题的解决依然采用同步块和wait
和notify
协调工作的方式,但是在本次作业中需要考虑到的同步问题比之前的作业都复杂得多,因为换乘请求的加入,除了调度器将请求分配给电梯之外,转运电梯还可能将请求重新发回给调度器,因此之前的线程结束逻辑就不再适用,另外等待队列的唤醒逻辑也需要重新设计,这些问题都是我在本次作业中遇到的挑战。
(3)调度器设计
在第七次作业中,针对换乘请求,我采用了静态分配的策略,即每当一个请求被调度器从输入线程接收到时,就会根据现有电梯的状况将其路线规划出来。如果该请求需要横向转运,会事先确定好其转运路线以及转运电梯,这样做的好处是实现简单,相比较动态分配不易出现错误,缺点是不能保证效率的最优。
调度整体上还是采用自由竞争的策略,各个电梯自由去争夺请求队列中的运输请求。每个请求第一次从输入线程被传入调度线程时,会判断该请求是否需要横向运输,如果需要,则会根据电梯队列中电梯的状态确定请求的转运电梯并将其首先运输到转运电梯所在楼层,之后被转运电梯运输到目的楼座后再由纵向电梯运输到目的地,因此每个请求最多被运输3次即可到达目的地。
(4)程序结构
UML图
整体架构相比前两次作业没有很大改动,类与类之间的依赖关系也都类似,架构整体上比较简单
复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
hwseven.BuildElevator.BuildElevator(int, RequestQueue, int, double, int, int, RequestQueue, ...) | 4 | 1 | 3 | 3 |
hwseven.BuildElevator.canTake(Person) | 10 | 4 | 4 | 7 |
hwseven.BuildElevator.canTrans(char, char) | 1 | 1 | 2 | 2 |
hwseven.BuildElevator.decideOpen() | 14 | 8 | 8 | 12 |
hwseven.BuildElevator.getNowFloor() | 0 | 1 | 1 | 1 |
hwseven.BuildElevator.getRollState() | 2 | 1 | 1 | 2 |
hwseven.BuildElevator.isEmpty() | 4 | 2 | 2 | 5 |
hwseven.BuildElevator.isEnd() | 0 | 1 | 1 | 1 |
hwseven.BuildElevator.run() | 1 | 1 | 2 | 2 |
hwseven.BuildElevator.setTarBuilding() | 2 | 1 | 2 | 2 |
hwseven.BuildElevator.stopOneBuilding() | 30 | 1 | 13 | 13 |
hwseven.BuildElevator.turnOneBuilding() | 35 | 1 | 13 | 17 |
hwseven.BuildElevator.updataMainTaskByInside() | 0 | 1 | 1 | 1 |
hwseven.BuildElevator.updateSubQ() | 4 | 1 | 4 | 4 |
hwseven.BuildElevator.work() | 22 | 5 | 9 | 12 |
hwseven.FloorElevator.FloorElevator(int, char, RequestQueue, int, double, RequestQueue, Schedule) | 0 | 1 | 1 | 1 |
hwseven.FloorElevator.canTake(Person) | 16 | 1 | 11 | 26 |
hwseven.FloorElevator.decideOpen() | 14 | 8 | 8 | 12 |
hwseven.FloorElevator.downOneFloor() | 21 | 1 | 11 | 11 |
hwseven.FloorElevator.getDirection() | 3 | 1 | 1 | 3 |
hwseven.FloorElevator.isEmpty() | 4 | 2 | 2 | 5 |
hwseven.FloorElevator.isEnd() | 0 | 1 | 1 | 1 |
hwseven.FloorElevator.run() | 1 | 1 | 2 | 2 |
hwseven.FloorElevator.setTarFloor() | 2 | 1 | 2 | 2 |
hwseven.FloorElevator.stopOneFloor() | 21 | 1 | 11 | 11 |
hwseven.FloorElevator.upOneFloor() | 21 | 1 | 11 | 11 |
hwseven.FloorElevator.updataMainTaskByInside() | 0 | 1 | 1 | 1 |
hwseven.FloorElevator.updateSubQ() | 4 | 1 | 4 | 4 |
hwseven.FloorElevator.work() | 37 | 4 | 13 | 16 |
hwseven.InputReq.InputReq(RequestQueue, ArrayList |
0 | 1 | 1 | 1 |
hwseven.InputReq.addObs(Schedule) | 0 | 1 | 1 | 1 |
hwseven.InputReq.isEnd() | 0 | 1 | 1 | 1 |
hwseven.InputReq.run() | 22 | 3 | 10 | 10 |
hwseven.MainClass.main(String[]) | 3 | 1 | 4 | 4 |
hwseven.Person.Person(PersonRequest) | 1 | 1 | 1 | 2 |
hwseven.Person.getBaseRequest() | 0 | 1 | 1 | 1 |
hwseven.Person.getNowBuild() | 0 | 1 | 1 | 1 |
hwseven.Person.getNowFloor() | 0 | 1 | 1 | 1 |
hwseven.Person.getTarFloor() | 0 | 1 | 1 | 1 |
hwseven.Person.getTransElevator() | 0 | 1 | 1 | 1 |
hwseven.Person.isArrive(int, char) | 3 | 2 | 2 | 3 |
hwseven.Person.isShouldTrans() | 0 | 1 | 1 | 1 |
hwseven.Person.setBaseRequest(PersonRequest) | 0 | 1 | 1 | 1 |
hwseven.Person.setNowBuild(char) | 0 | 1 | 1 | 1 |
hwseven.Person.setNowFloor(int) | 0 | 1 | 1 | 1 |
hwseven.Person.setShouldTrans(boolean) | 0 | 1 | 1 | 1 |
hwseven.Person.setTarFloor(int) | 0 | 1 | 1 | 1 |
hwseven.Person.setTransElevator(BuildElevator) | 0 | 1 | 1 | 1 |
hwseven.Printer.println(String) | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.RequestQueue() | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.addRequest(Person) | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.getHead() | 5 | 2 | 4 | 5 |
hwseven.RequestQueue.getOneRequest() | 5 | 2 | 4 | 5 |
hwseven.RequestQueue.getRequests() | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.isEmptyWithOutNotify() | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.pop() | 0 | 1 | 1 | 1 |
hwseven.RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
hwseven.Schedule.Schedule(RequestQueue, InputReq, ArrayList |
0 | 1 | 1 | 1 |
hwseven.Schedule.forwardBuild(int, Person) | 0 | 1 | 1 | 1 |
hwseven.Schedule.forwardFloor(char, Person) | 5 | 1 | 5 | 5 |
hwseven.Schedule.isDead() | 0 | 1 | 1 | 1 |
hwseven.Schedule.run() | 48 | 9 | 17 | 20 |
Class | OCavg | OCmax | WMC | |
hwseven.BuildElevator | 4.27 | 15 | 64 | |
hwseven.FloorElevator | 4.43 | 10 | 62 | |
hwseven.InputReq | 3.25 | 10 | 13 | |
hwseven.MainClass | 4 | 4 | 4 | |
hwseven.Person | 1.14 | 2 | 16 | |
hwseven.Printer | 1 | 1 | 1 | |
hwseven.RequestQueue | 1.4 | 3 | 14 | |
hwseven.Schedule | 4.8 | 16 | 24 |
在本次作业中各个类的复杂度都明显提高,类中方法的复杂度也明显提高,这是由于请求不断在各个线程中被传送,在电梯中被反复运输导致的。
(5)Bug分析
强测阶段
强测阶段出现的Bug有两类
- 需要横向转运的电梯通过转运电梯运输后没有更改当前所在位置以及目的楼层导致出现bug
- 线程结束逻辑存在Bug,导致CPU资源被占用进而超时
互测阶段
互测阶段出现的Bug也是由于线程结束逻辑错误导致的
时序图
主要流程如下
- 由Main线程创建输入线程、调度器线程、总请求队列、5个纵向请求队列、10个横向请求队列以及6个初始电梯线程
- 输入线程不断从标准输入中读取请求,判断请求的种类,如果是增加电梯请求则创建新的电梯线程,若为运输请求则加入总请求队列
- 调度器线程不断从总请求队列中读取请求并对需求进行拆解,之后分配给相应的请求队列
- 电梯线程不断从所在的请求队列中读取请求并进行运输,如果请求还需要转运,则将转运后的请求更新状态并重新返回总请求队列
- 如果输入线程读到结束符,则输入线程停止工作并设置相应标记位
- 如果所有电梯内请求都为空且所有请求队列都为空则将电梯可以结束的Tag属性置为True
- 如果输入线程结束工作且主请求队列为空且所有电梯的结束Tag都为True,则调度器线程结束工作并将所有请求队列的isEnd置为True
- 若电梯所在请求队列isEnd为True,则电梯线程停止工作
可扩展性分析
在第七次作业中,已经建立好了请求在输入线程、调度器线程、电梯线程之间的转送逻辑,因此就算将来不断对电梯的功能或者运输维度进行拓展,整体思路大体就是不断拆解请求的运输过程并将不同阶段的请求分配到各个电梯的请求队列当中,所以架构的可扩展性比较好。
HACK策略
由于本单元的主题是多线程,因此我主要针对程序的线程安全问题进行hack,比如同一时刻输入大量请求,同时大量输入同一类的请求等,但是可能是因为自己构造的数据不够强,hack的成功率并不高。与第一单元的hack策略相比,本单元着重考虑线程安全方面的问题而不是程序算法逻辑上的问题,因此对于边界情况、临界条件等第一单元的hack重点在本单元的作业中并未体现。
心得体会
线程安全
在完成本单元的作业时,在设计架构和编写电梯运输算法时并没有觉得困难,遇到的问题大多跟线程安全有关,在强测和互测中遇到的Bug绝大多数也与线程安全相关,通过这一单元的历练,算是入了多线程编程的门,学会了使用同步块和加锁来解决线程安全的问题。在多个线程进行信息交互时,需要正确找到临界资源并保证其访问的安全性,另外不能滥用同步块和notify语句。
层次化设计
在层次化设计上,尽可能的将各个类的功能独立出来,防止过多的耦合,例如将请求的分配从电梯中分离出来,建立独立的调度器类,电梯类只负责运输有关的逻辑。除此之外,建立了一套比较完善的输入线程-调度器-电梯-输出线程的设计架构,可扩展性较强,在经历了两个单元的OO历练之后,已经能对工程项目的整体架构有一个比较清晰的认识,能够在建立好架构之后再进行有效开发,达到事半功倍的效果。