面向对象第二单元总结
第二单元的作业是电梯调度,模拟有五个座的大楼中的电梯调度情况。与上一单元相同,本单元三次作业为迭代开发,在第五次作业中实现每座一部电梯的调度;在第六次作业增加横向电梯,并实现电梯数量的动态调整;在第七次作业中设置电梯的容量、速度、停靠位置等参数,并实现换乘的功能。
第五次作业
结构分析
由于主线程在创建线程、初始化对象后没有其它任务,本单元作业中可将其作为输入线程继续使用,这样可使输入线程掌握其它线程与对象的信息,在投放需求、结束线程等方面均较为方便。本次作业的需求较为简单,电梯只有固定的五部,乘客需求只有同一座内的纵向接送,所有需求均可依据楼座信息对应到唯一的电梯。代码架构采用生产者消费者模式,对于每一座,输入线程为生产者,电梯为消费者,读入的需求保存在Table中,并由电梯线程取出。
由于乘客需求分布复杂,且不能预测未来一段时间内的需求情况,无法保证一种运行策略具有绝对的优势,因此我没有在策略上进行太多分析与计算。电梯运行策略基本采用look算法,即优先考虑当前运行方向上最远的需求、并沿途捎带乘客,同时对算法进行修改:开门时将反方向乘客一并捎带。例如电梯上行至某一楼层时,若将乘客全部接入,之后下行至该层时便无需开门接人,由此可节省0.4s运行时间。该方法也存在缺陷:反向乘客长时间占用电梯容量空间,降低正向运输效率,在乘客密集时反而会增加运行时间。
此外,多线程运行的安全性为本次作业的一个重点,由于Table是线程间的共享资源,其方法都需要加锁,而电梯线程遍历其中所有乘客需求时,则为相关代码块加锁,以防遍历过程中输入线程修改其中的内容而导致错误。由于官方提供的输出包不保证线程安全,需要额外创建Outputer对象管理各线程的输出。
方法复杂度
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Elevator.checkDirection() | 12.0 | 4.0 | 6.0 | 9.0 |
Elevator.close() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.Elevator(int, char, Table, Outputer) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.exchange() | 9.0 | 4.0 | 6.0 | 9.0 |
Elevator.move() | 3.0 | 1.0 | 2.0 | 3.0 |
Elevator.open() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.passengerIn() | 5.0 | 3.0 | 3.0 | 4.0 |
Elevator.passengerOut() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.run() | 14.0 | 4.0 | 8.0 | 9.0 |
MainClass.main(String[]) | 8.0 | 3.0 | 4.0 | 8.0 |
Outputer.arriveOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.closeOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.inOutput(int, char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.openOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.outOutput(int, char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.addRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.finish() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.getHighest() | 4.0 | 2.0 | 3.0 | 4.0 |
Table.getLowest() | 4.0 | 2.0 | 3.0 | 4.0 |
Table.getRequests() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.isFinished() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.ready(int) | 3.0 | 3.0 | 2.0 | 3.0 |
Table.removeRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 66.0 | 40.0 | 54.0 | 70.0 |
Average | 2.87 | 1.74 | 2.35 | 3.04 |
类复杂度
class | OCavg | OCmax | WMC |
---|---|---|---|
Elevator | 3.44 | 7.0 | 31.0 |
MainClass | 8.0 | 8.0 | 8.0 |
Outputer | 1.0 | 1.0 | 5.0 |
Table | 2.0 | 4.0 | 16.0 |
Total | 60.0 | ||
Average | 2.61 | 5.0 | 15.0 |
电梯类的复杂度较高,原因是需要在其中实现运行策略,完成开关门判断、确定运行方向等任务。
BUG修复
第五次作业在强测中有一个点超时,原因是运行策略存在问题:1. 当某一层只有反向乘客时也会开门接人,这样就是无故浪费电梯资源;2. 运行方向判断有误,导致有时电梯本应转向,却会继续运行到底层或顶层,无故增加行程。
第六次作业
结构分析
第六次作业新增横向电梯与电梯数量的动态调整,其中横向电梯的要求较为简单:将楼座视为楼层,即可当作纵向电梯运行。此外,由于楼座为循环分布,甚至可以让横向电梯保持单向运行,省去方向的判断。
新增电梯后,将会出现多部电梯在相同区间运行的情况,此时需求的分配思路大致分为两种:一是让多个电梯线程在同一托盘内竞争;二是新开一个线程,将需求分配至电梯对应的托盘中。由于初始条件中每个楼座能保证至少一部电梯,我用Scheduler分配纵向需求,而对横向电梯采用自由竞争的策略。分配时,首先随机选择一部电梯作为目标,后将其与其他电梯比较,当存在更合适的电梯,即等待人数明显较少或正好能搭载待分配需求时,则调整目标电梯。Scheduler与电梯的通信通过Table实现,前者将需求放入Table,而后者将自身的部分状态如运行方向、当前楼层等写入Table中。访问相关资源时,只需将Table类中的相应方法加锁即可保证线程安全。
方法复杂度
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
HorizontalElevator.checkDirection() | 24.0 | 4.0 | 6.0 | 10.0 |
HorizontalElevator.close() | 0.0 | 1.0 | 1.0 | 1.0 |
HorizontalElevator.exchange() | 9.0 | 4.0 | 6.0 | 9.0 |
HorizontalElevator.HorizontalElevator(int, int, Table, Outputer) | 0.0 | 1.0 | 1.0 | 1.0 |
HorizontalElevator.move() | 7.0 | 1.0 | 2.0 | 5.0 |
HorizontalElevator.open() | 1.0 | 1.0 | 2.0 | 2.0 |
HorizontalElevator.passengerIn() | 5.0 | 3.0 | 3.0 | 4.0 |
HorizontalElevator.passengerOut() | 3.0 | 1.0 | 3.0 | 3.0 |
HorizontalElevator.run() | 13.0 | 4.0 | 7.0 | 8.0 |
MainClass.main(String[]) | 27.0 | 3.0 | 12.0 | 12.0 |
Outputer.arriveOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.closeOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.inOutput(int, char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.openOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.outOutput(int, char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Scheduler.addTable() | 1.0 | 1.0 | 1.0 | 2.0 |
Scheduler.finish() | 1.0 | 1.0 | 2.0 | 2.0 |
Scheduler.run() | 35.0 | 10.0 | 10.0 | 14.0 |
Scheduler.Scheduler(Table) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.addRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.finish() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.getFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.getHighest(int) | 8.0 | 2.0 | 4.0 | 5.0 |
Table.getLowest(int) | 8.0 | 2.0 | 4.0 | 5.0 |
Table.getRequests() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.isFinished() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.isGoUp() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.ready(char) | 3.0 | 3.0 | 2.0 | 3.0 |
Table.ready(int, boolean) | 4.0 | 3.0 | 3.0 | 4.0 |
Table.removeRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.setFloor(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.setGoUp(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.takeFirst() | 0.0 | 1.0 | 1.0 | 1.0 |
VerticalElevator.checkDirection() | 12.0 | 6.0 | 8.0 | 10.0 |
VerticalElevator.close() | 0.0 | 1.0 | 1.0 | 1.0 |
VerticalElevator.exchange() | 10.0 | 3.0 | 6.0 | 7.0 |
VerticalElevator.move() | 3.0 | 1.0 | 2.0 | 3.0 |
VerticalElevator.open() | 1.0 | 1.0 | 2.0 | 2.0 |
VerticalElevator.passengerIn() | 11.0 | 5.0 | 6.0 | 8.0 |
VerticalElevator.passengerOut() | 3.0 | 1.0 | 3.0 | 3.0 |
VerticalElevator.run() | 13.0 | 4.0 | 7.0 | 8.0 |
VerticalElevator.VerticalElevator(int, char, Table, Outputer) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 202.0 | 84.0 | 121.0 | 149.0 |
Average | 4.81 | 2.0 | 2.88 | 3.55 |
类复杂度
class | OCavg | OCmax | WMC |
---|---|---|---|
HorizontalElevator | 4.0 | 10.0 | 36.0 |
MainClass | 11.0 | 11.0 | 11.0 |
Outputer | 1.0 | 1.0 | 5.0 |
Scheduler | 4.0 | 11.0 | 16.0 |
Table | 1.86 | 5.0 | 26.0 |
VerticalElevator | 3.78 | 8.0 | 34.0 |
Total | 128.0 | ||
Average | 3.05 | 7.67 | 21.33 |
第七次作业
结构分析
第七次作业中电梯参数的设置比较简单,由于我在前两次作业中已经将容量等信息作为字段保存在了电梯类中,因此只需在构造器中添加相应参数的赋值,即可实现这一要求。
对于横向电梯的可达性,在创建电梯时便将相关信息进行解析,用数组保存可以开关门的楼座。对于每个楼层,由于所有横向电梯共享同一Table,将开关门信息同时写入相应Table,即可避免访问电梯线程对象的字段,否则规划换乘策略时,需要对电梯频繁加锁,影响程序的性能。
换乘策略在读取乘客需求时确定。由于官方输入包中的PersonRequest类不能保存换乘路线,我新建了Passenger类继承PersonRequest类,并用数组存储各换乘节点的楼层、楼座信息。为减少开关门次数、提升性能,对读入的需求首先判断是否存在不换乘策略,若有则可直接确定路线,否则需要规划横向换乘楼层。对于每个楼层,从Table中读取上一步中保存的可达性信息,换乘条件为该楼层存在至少一部电梯,能在起点与终点楼座开门;在可换乘楼层中,计算每种策略需要移动的楼层数,在其中找到最佳方案。该规划方法属于静态规划,好处是简单明了,无需反复修改路线,且计算量较少,而缺点是无法实现动态规划的灵活:例如,如果在读取所有乘客需求后才加入若干横向电梯,则新电梯将无法用于路线的规划。此外,路线的规划不能实现多次横向换乘,但考虑到增加换乘次数对开关门时间的需求,除非构造极端数据,否则这一点带来的影响将微乎其微。
换乘带来的另一个影响,是线程终止条件的判断。在前两次作业中,输入结束时即可设置终止信息,电梯完成Table中的全部任务后便可终止,而本次作业中若沿用该方法则可能出现如下情况:电梯线程结束后,有换乘需求结束上一阶段移动并被投入该电梯的等待队列,则该需求将无法完成。对此需要统计读入与完成的需求数量,在输入结束之外,增加完成全部已读入需求作为终止条件,此时输入线程向各Table中写入终止信息,并由其他各线程获取。
经过三次迭代开发,程序的时序图如下:
方法复杂度
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
HorizontalElevator.checkDirection() | 43.0 | 9.0 | 8.0 | 16.0 |
HorizontalElevator.close() | 0.0 | 1.0 | 1.0 | 1.0 |
HorizontalElevator.exchange() | 10.0 | 4.0 | 7.0 | 10.0 |
HorizontalElevator.HorizontalElevator(ElevatorRequest, Table, Outputer, ArrayList) | 3.0 | 1.0 | 3.0 | 3.0 |
HorizontalElevator.HorizontalElevator(int, int, Table, Outputer, int, double, int, ArrayList) | 3.0 | 1.0 | 3.0 | 3.0 |
HorizontalElevator.move() | 7.0 | 1.0 | 2.0 | 5.0 |
HorizontalElevator.open() | 0.0 | 1.0 | 1.0 | 1.0 |
HorizontalElevator.passengerIn() | 6.0 | 3.0 | 4.0 | 5.0 |
HorizontalElevator.passengerOut() | 7.0 | 1.0 | 4.0 | 4.0 |
HorizontalElevator.run() | 26.0 | 6.0 | 9.0 | 11.0 |
MainClass.initializeVerticalElevators(ArrayList, ArrayList, Outputer, HashMap) | 1.0 | 1.0 | 2.0 | 2.0 |
MainClass.main(String[]) | 23.0 | 3.0 | 13.0 | 13.0 |
Outputer.arriveOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.closeOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.finished() | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.finishOne() | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.inOutput(int, char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.openOutput(char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.outOutput(int, char, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Outputer.requestOne() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.arrive() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.getCurFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.getCurPlace() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.getNextFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.getNextPlace() | 0.0 | 1.0 | 1.0 | 1.0 |
Passenger.Passenger(PersonRequest, HashMap) | 30.0 | 12.0 | 16.0 | 18.0 |
Scheduler.addTable() | 1.0 | 1.0 | 1.0 | 2.0 |
Scheduler.finish() | 1.0 | 1.0 | 2.0 | 2.0 |
Scheduler.run() | 35.0 | 10.0 | 10.0 | 14.0 |
Scheduler.Scheduler(Table) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.addRange(ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.addRequest(Passenger) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.finish() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.getFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.getHighest(int) | 4.0 | 2.0 | 3.0 | 4.0 |
Table.getLowest(int) | 4.0 | 2.0 | 3.0 | 4.0 |
Table.getRange() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.getRequests() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.isFinished() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.isGoUp() | 0.0 | 1.0 | 1.0 | 1.0 |
Table.ready(char, ArrayList) | 4.0 | 3.0 | 4.0 | 5.0 |
Table.ready(int, boolean) | 4.0 | 3.0 | 3.0 | 4.0 |
Table.removeRequest(Passenger) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.setFloor(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.setGoUp(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Table.takeFirst() | 0.0 | 1.0 | 1.0 | 1.0 |
VerticalElevator.checkDirection() | 12.0 | 6.0 | 8.0 | 10.0 |
VerticalElevator.close() | 0.0 | 1.0 | 1.0 | 1.0 |
VerticalElevator.exchange() | 15.0 | 3.0 | 8.0 | 9.0 |
VerticalElevator.move() | 3.0 | 1.0 | 2.0 | 3.0 |
VerticalElevator.open() | 0.0 | 1.0 | 1.0 | 1.0 |
VerticalElevator.passengerIn() | 11.0 | 5.0 | 6.0 | 8.0 |
VerticalElevator.passengerOut() | 8.0 | 1.0 | 5.0 | 5.0 |
VerticalElevator.run() | 13.0 | 4.0 | 7.0 | 8.0 |
VerticalElevator.VerticalElevator(ElevatorRequest, Table, Outputer, HashMap) | 0.0 | 1.0 | 1.0 | 1.0 |
VerticalElevator.VerticalElevator(int, char, Table, Outputer, int, double, HashMap) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 274.0 | 116.0 | 165.0 | 200.0 |
类复杂度
class | OCavg | OCmax | WMC |
---|---|---|---|
HorizontalElevator | 5.1 | 16.0 | 51.0 |
MainClass | 6.5 | 11.0 | 13.0 |
Outputer | 1.0 | 1.0 | 8.0 |
Passenger | 3.17 | 14.0 | 19.0 |
Scheduler | 4.0 | 11.0 | 16.0 |
Table | 1.625 | 4.0 | 26.0 |
VerticalElevator | 3.6 | 8.0 | 36.0 |
Total | 169.0 | ||
Average | 3.02 | 9.29 | 24.14 |
BUG修复
本次作业的bug在于开发换乘功能时产生错误,具体为规划路线时,可直达需求设置完成后未及时返回,导致已写入的值被覆盖,从而造成不符合输入要求、不在电梯设计搭载范围内的需求。
Hack与调试策略
构造数据时,我的重点在多线程的特点与电梯的功能。针对输出不安全,可构造数据使大量乘客同时进出电梯、造成多个线程同时输出信息,诱发错误。通过长时间的输入空白,可检查出一些线程轮询错误。此外,由于第七次作业中的横向电梯有可达性限制,可针对性构造数据、诱发路线规划问题或开门位置的错误。
多线程的调试比较困难,一是难以对所有线程进行断点调试而又模拟运行时的状态,二是每次运行都不太相同,导致问题复现困难。调试程序时,我花了不少精力分析代码结构、思考潜在的问题,如此纠正了一些错误;之后构造一些数据进行测试,发现了诸如电梯无限上升、下降的bug。好在我对多线程上的架构一直比较谨慎,没有发现所谓的玄学bug,所有的bug均能够稳定复现,省下了不少时间。
心得体会
本单元电梯调度作业是我第一次接触多线程设计,而通过三次训练,我对线程安全这一多线程开发无法避免的问题有了较为深刻的体会,也熟悉了一些设计模式。本单元作业对何时加锁、加在哪里的问题并不是非常严格,只要访问共享资源时保证加锁,就基本能够保证线程的安全,而性能优化的重点主要还是在调度策略上,毕竟访问资源的时间在0.4s为单位的sleep时间面前可以说是微不足道。而加锁的问题还是值得深入分析、推敲细节,毕竟锁对程序的性能还是存在着不小的影响(否则Java应该已经对所有方法都无脑加锁了)。我对开发模式的学习来自《图解Java多线程设计模式》与两次上机练习,作业的架构也是基于其中的知识,从第五次作业开始便采用生产者消费者模式,在第七次作业中引入流水线的思想。设计模式并非高深莫测、晦涩难懂,而是贴近实际、易于理解,但何时采用哪种模式,以及如何构建模型,则还需多加学习与思考。与单线程相比,多线程设计确实具有其独特的魅力,经过了本单元的训练,我也深感其博大精深。