OO 第二单元总结
第一次作业
题目要求
A、B、C、D、E五栋楼均有10层,每栋内有一座纵向电梯,输入为同一楼座不同楼层的请求。需要模拟电梯的运行,并输出开门、关门、乘客进出信息。
设计思路
电梯策略部分,由于没有经过充分的思考,采取了一种极为愚蠢且诡异的方案,这使得后续的迭代不方便且产生了致命的bug。电梯完全按照它的一个属性toDoList执行运动和人员进出,toDoList是一个Move型的ArrayList,其中Move类中含有一个人的id、一个楼层、和一个boolean值表示进或出。Strategy类通过接受电梯楼层、等待队列等属性更改toDoList来影响电梯的行动。当产生新的请求时,调用getStrategy()方法将新的请求转化为两个Move并插入原有的toDoList。
电梯运行的run()方法的循环中,先判断是否输入已结束且输入队列为空且toDoList为空,若是,则结束。然后调用getStrategy()方法,若请求队列不为空则将新请求插入toDoList。若调用getStrategy()方法后toDoList依然为空,则continue。这会导致没有新请求且没有待处理任务时出现轮询,将在bug分析部分详细说明。然后,电梯从toDoList中取出第一个Move,向其楼层移动,若已到达该楼层,则让此Move中的人进出,并从toDoList中移除此Move。
架构分析
类图
使用toDoList和Move决定电梯运行的策略并不利于后续的迭代,因为电梯的运行轨迹是在接收到乘客请求后立即决定的,不够灵活。若后续作业中出现移除电梯等要求,迭代将变得十分困难。
时序图
调度器设计
调度器负责操作一个包含输入请求的请求队列和五个不同电梯的请求队列。输入线程向输入队列中放入新请求,调度器调用getOneRequest()方法从输入队列中获得请求,并根据其楼座将其放入相应电梯的请求队列。当输入结束时,getOneRequest()方法返回null,调度器调用setEnd()方法将所有电梯的请求队列标志为结束。
同步块设置
唯一的共享对象是RequestQueue。RequestQueue中的所有方法,包括添加请求、取出请求、判断是否为空等都加上了synchronized关键字,防止两个线程同时调用。
另外,为了保证不出现输出时间戳不递增的问题,设置了OutputThread类,其中的println方法加上了synchronized关键字。
Bug分析
中测过程中出现了一个轮询的bug。由于电梯的run()方法中,每次循环中需要调用一次getStrategy()方法,用isEmpty()方法检查是否有新的请求并插入toDoList中,这导致了一个轮询的bug。当没有新请求且没有待处理任务时,就会出现轮询。解决方法是增加了一个isEmptyWithWait()方法,与isEmpty()方法的唯一区别是在返回前增加了wait()。在电梯的run()方法中,先调用一次isEmpty()方法,然后若电梯当前没有需要处理的任务,则调用isEmptyWithWait()方法,直到出现新请求再继续。若有心请求,则不调用isEmptyWithWait()方法,处理当前请求。
由于设计思路不合适,出现了一个会导致超载的致命bug。由于电梯完全按照toDoList执行运动和人员进出,电梯无法判断所乘人员数是否超载。getStrategy()方法需要判断新插入的Move是否会产生超载。在原有方法中,只判断了插入Move后是否会使下一步超载,而没有考虑再后面的超载情况。后来在Move类中新增了capacity属性,用于存储电梯执行这个Move后剩余的可乘人数。每次插入Move前先遍历整个toDoList,找到最早的插入后不会使后续操作超载的位置,再插入Move,然后更新受到影响的Move中的capacity属性。
由于第二单元中自行构造测试数据后的检验比第一单元困难,我自己的程序又存在很大问题,所以没有试图找到其他人的bug,只是用室友的一组数据提交了一次。
第二次作业
题目要求
增加了横向电梯、增加横向或纵向电梯指令、横向请求,仅当某楼层的横向电梯存在时才会出现本楼层的横向请求。
设计思路
横向电梯和纵向电梯可以采取类似的策略,没有太大问题。对横向电梯、横向电梯策略等建立了与纵向电梯类似的类或接口。由于同一个楼层或楼座内可以有多部电梯,建立了一个新的ElevatorGroup类用于管理同一楼层或楼座的电梯。Controller负责管理所有的ElevatorGroup。当ElevatorGroup接收到请求时,将请求均匀地分配给其中的电梯,采用主从模式。
架构分析
类图
加入横向电梯后,本次作业的类图变得比较混乱。应该将横向和纵向电梯的更多属性和方法放入其共同父类中。
本次作业中新增的ElevatorGroup类对处理同一楼座或同一楼层的请求时比较方便,便于向下一次作业的迭代。
时序图
调度器设计
由于增加了增加电梯请求,而我的RequestQueue是针对自己新建的Person类设计的,无法盛放ElevatorRequest,采取了一种愚蠢的方法,删除了InputThread类,由调度器直接接收输入。调度器由管理电梯请求队列变为管理ElevatorGroup,调度器中使用一个以楼层或楼座作为键的HashMap存储所有的ElevatorGroup。当调度器接收到一个增加电梯请求时,先判断该电梯的楼座或楼层所对应的ElevatorGroup是否存在,若不存在,则新建ElevatorGroup并加入调度器中。然后调用ElevatorGroup的addElevator()方法加入新电梯。当调度器接收到一个人的请求时,从存储ElevatorGroup的HashMap中取出相应楼层或楼座的ElevaoterGroup并向其中加入请求。
同步块设置
本次作业没有增加新的同步块。
bug分析
本次作业在强测和互测中均没有出现bug,也没有试图找到其他人的bug。
第三次作业
题目要求
纵向电梯的容量和速度可定制,横向电梯的容量、速度和可到达楼座可定制。乘客请求的出发和到达楼座、出发和到达楼层可能均不同。
设计思路
单部电梯运行策略不变。调度器采用流水线模式,当一部电梯完成一个请求的一部分后,将请求放回调度器继续完成。当出现不同楼座之间的请求时,由调度器决定其运动路线。找到使纵向运行距离最短的楼层作为换乘楼层。Person类中新增setInterchange()方法,用于设置换乘地点。然后将新请求放入其首先需要乘坐的电梯中。
另外,为了使程序在正确的时刻结束,参考实验增加了计数器PersonCounter类,但与实验中实现方法不同。其作用是对正在处理的乘客请求进行记数,当输入结束且正在处理的乘客请求数量为0时,才结束程序。
架构分析
类图
总体上采用了流水线模式,调度器和计数器采用了单例模式。
时序图
调度器设计
为了实现流水线模式,便于电梯将未完成请求加回调度器,将调度器改为采用单例模式,不再继承Thread类。为使电梯能够向调度器中加入请求,对调度器的addPerson()方法进行重载。原来InputThread的工作由主线程完成。主线程接收请求后调用调度器的addPerson()和addElevator()方法加入电梯或乘客请求。电梯
同步块设置
计数器中,add()和remove()、remove()和checkEnd()方法可能被同时调用,且这三个方法中add()和remove()需要写count属性,checkEnd()方法需要读count属性,所以add()、remove()、checkEnd()方法均加上synchronized关键字。
调度器中,由于电梯将完全完成的请求加回调度器时调用addPerson()方法需要遍历调度器中所有ELevatorGroup,主线程增加电梯时调用addElevator()方法需要写入ElevatorGroup,addPerson()和addElevator()方法中均加入synchronized关键字。
bug分析
由于电梯向调度器加回请求时,调度器加回请求时换乘电梯需要反应时间,且对于不同容量和速度的电梯没有根据其容量和速度做特殊处理,所有测试点的时间都比较长,出现了两个RTLE。
在互测中,只提交了自己测试时的三组数据,共hack中4次。没有被别人发现bug。
心得体会
线程安全
为避免线程不安全等问题,需要在设计前明确所有的共享对象、访问共享对象的线程。当存在多个共享对象和多个线程时,要考虑多个线程访问共享对象的顺序,避免出现死锁。
层次化设计