面向对象第二单元总结
第二单元作业(电梯调度)总结
一、程序结构及多线程控制的分析
第一次作业:
·类图
·复杂度
本单元的第一次作业是要求实现一个支持捎到功能的多线程调度电梯。
在本次作业中,我一共创建了三个线程,分别在InputHandler类、Dispatcher类和Storey类中。
InputHandler类中的线程负责不断从控制台中读入请求并且存入到RequestQueue中。
Dispatcher类中的线程负责不断地从RequestQueue中抓取读入的请求,并将其存入dispatcher类中的队列保存,以便storey从中抓取请求。
Storey中的线程主要有两个功能,其一为控制电梯执行楼层类中保存的请求,其二为在电梯运行的特定时间结点从dispatcher的队列中查找捎带请求或是设置主请求。
下面为Storey类的属性和StoreyRequest类的属性:
在Storey类中,我用一个二维的Vector模拟实现了现实世界中的楼层storey。外层的Vector用于模拟楼层-3到16,内层的Vector则是存储了每一层中需要执行的任务,其中的任务是用StoreyRequest类存储的。每次storey从dispatcher抓取到合适的请求后,便对请求进行拆解,然后将拆解后的请求以StoreyRequest的形式插入到storey中。电梯在storey的控制下,每到一层便从storey中所对应的楼层Vector中取出任务并执行上下客的操作。
在elevator与dispatcher之间加入楼层类storey后,将请求进一步地拆解然后加入到用二维Vector实现的storey中,能够极大地降低elevator与dispatcher之间的耦合度。这样做为后续功能的实现与调试带了极大的方便。
由于在指导书中对电梯的调度算法并没有给出严格的限制,所以我也并没有严格地按照ALS调度算法实现捎带。我的调度算法是在设置好主请求后,只要新请求的目标方向与当前电梯的运动方向一致,便可以实现请求的捎带。但是由于我在实现时没有实现更换主请求的功能,所以在一些情况下并不能实现类似LOOK算法的捎带。比如针对如下的两条请求:1-FROM-15-TO-1、2-FROM-16-1,我的电梯在到达15层接上1号请求后便会直接转向,导致请求2被忽略。调度算法上的缺陷也导致我在性能上有许多损失。
第一次作业中我创建了三个线程,线程安全的问题主要有两对。其一为InputHandler与Dispatcher对RequestQueue的存取操作之间存在线程安全的问题,其二为Dispatcher向dispatchQueue中存储请求与Storey类从dispatchQueue中抓取请求之间存在线程安全的问题。而Storey中的Vector容器由于请求的插入与电梯对请求的消耗都是在Storey中的一个线程完成的,二者的执行总是异步的,故不存在线程安全问题。
针对第一个问题,我用对RequestQueue类中的存取方法加上关键字synchronized的方法保证了线程的安全。针对第二个问题,我是通过对Dispatcher中的方法加上关键字synchronized,在storey抓取请求时调用Dispatcher类中的方法去抓取请求来保证线程安全的。
第二次作业:
·类图
本单元的第二次作业是实现
第三次作业:
·类图
·复杂度
本单元的第三次作业是实现
第三次作业中,我新增了一个二维的ArrayList队列trueRequest,其中存储的是每个id对应的初始请求经split拆分后所生成的请求队列。Storey类在从trueRequest中抓取请求时,只会扫描每个id请求队列的第一项,在某部电梯完成该任务后,将执行完的请求从头部弹出,以便其他电梯继续执行剩下的一个请求。当id对应的请求队列为空后,从trueRequest队列中移除这个id队列的请求即可。利用队列的结构可以很好地解决拆分后请求执行顺序的问题。
在Split中对请求的拆分与任务的分配我做的十分粗糙。最后也仅仅是采取枚举的方式,做了一些简单地静态优化。比如对于一定楼层区间地请求,在15楼实现A电梯和B电梯可以换乘,在5楼实现实现B电梯和C电梯地换乘等等。
在拆分请求时,我没有考虑各电梯实际的运行状态,只是将请求按照一定的检查顺序进行固定的分割。这样的拆分方式,极容易导致各电梯之间工作量的不平衡,尤其是B电梯任务量过重,以及电梯在执行请求时会出现绕路的情况。但是强测结果出来后,发现电梯的调度性能分也没有很差,也获得了不错的分数。
针对在加入电梯后,同类请求可由不同id号但是类型相同的电梯进行服务,在Request类中再加入一个int serverNumber。一旦一个请求被一个电梯选为主请求或者捎带上后,就将队列中请求的serverNumber标记为电梯的id号,这样就能避免一个请求被多个电梯服务的问题了。
第三次作业的架构与第二次作业基本一致,可能出现线程安全的地方基本一致,解决方法也是一样的。
在复杂度方面,虽然其他模块的复杂度没有太多的提升,但是由于Split模块的出现引入了大量的if-else分支语句,导致了程序的独立路径条数大幅度增加,复杂度进一步提升。因此,在降低程序复杂度方面还有很多工作要去做。
二、出现的bug及如何找bug
在分析具体的bug之前,我先分析一下后两次作业中出现的比较影响电梯性能的问题。这个问题就是我的电梯在每到一个楼层后,如果楼层类里有请求的话,就会打开电梯门,在开门之后再对当前楼层里的请求进行扫描,决定是否执行上下客的任务。但是这样做常常会出现开门之后,在扫描完楼层队列后,发现并没有要执行的任务,然后再把电梯门关上,继续运行的情况。比如当前楼层队列里仅存储了一个出的请求,但是此时发现请求还没有进来,于是出的请求便不会执行。这样就导致了无效开关门情况的出现。每出现一次这种 无效开关门就会损失至少0.8s的时间,可想而知这个问题会对自己的性能分带来多大的影响。
第一次作业的bug分许:
在公测和互测中没有被测出bug,也没有找出同屋人的bug
第二次作业的bug分析:
在公测和互测中没有被测出bug,也没有找出同屋人的bug
第三次作业的bug分析:
在公测中没有被测出bug,在互测中被找出了若干个同质bug,同时找出了同屋三个人的bug。
第三次作业中出现的bug在于对指导书的错误解读
我理解成由于本次作业最多只有6部电梯,所以新增电梯的id号只能为X1,X2,X3,于是在新加入电梯时就进行了硬编码,如果电梯的id号不是X1,X2,X3,就直接报错退出了程序
现在反思后,觉得自己当时的处理方式不妥当,就算id号有问题,也不应该直接退出,完全是可以继续服务的,这样直接暴力停掉电梯的行为实在是不可取的。直接退出的处理办法是对程序鲁棒性的极大削弱。
不幸中的万幸时强测中的样例并没有针对电梯的id号进行测试,自己也算是躲过了一个大雷。
如何找别人的bug:
本人仍然是根据一些可能预见的坑点,手工构造一些测试样例去测试别人的bug。比如在第三次作业中,我把测试样例的楼层局限在-3层到4层之间,在这些楼层之间编写大量的请求进行测试,发现也能炸出别人的一些bug,取得了还不错的效果,找到了同屋人线程无法终止以及CPU时间超时的BUG。
三、心得体会
线程安全:
多线程编程时最需要注意解决的就是线程安全的问题。为了解决线程安全问题,我所采取的方法就是对可能产生线程安全问题的方法加上synchronized锁,以实现对共享数据的互斥访问。当然,由于对多线程的锁机制了解还不够深,我还是采取了最原始的锁方法。在以后的学习中,我需要进一步加强对锁机制的理解与应用,在保证线程安全的同时,使得程序的效率能够进一步提高。
设计模式:
在本单元的三次作业中,我的设计模式应该是一直延续了生产者消费者的模式。InputHandler类不断地抓取请求并放入dispatcher中,storey不断从dispatcher中抓取请求并控制电梯消耗。虽然在完成这单元的作业中,对生产者消费者模式有了比较深刻地认识,但是对于观察者模式等其他设计模式并没有真正地使用过。因此,在以后的OO作业中,希望自己能学习和应用到更多的设计模式。