OO第二单元总结
基本思路
三次电梯设计的迭代开发中,基本思路与框架几乎相同。
Elevator内有两个容器,personIn和tasks,分别表示在电梯上的乘客与电梯将要接的乘客。电梯运行的方向由电梯方法setDirection决定,是否有人需要向下电梯则遍历两个容器比较楼层得知。电梯方向优先由tasks决定,即如果有已接收但还未上电梯的请求,电梯就会往他的方向移动。(当然中途经过乘客的目的地的也是会放他下电梯的)。这样设计就需要保证分配给电梯的任务之间具有较好的协调性,需要避免为了接入一个乘客而将载着满电梯的人乱跑的情况。
由电梯自己从WaitPersons大盘子中获取乘客。WaitPersons中包含两个容器,分别收纳向上的请求与向下的请求并按照起始楼层排序。获取请求主要分为两种情况:电梯空乘(两个容器都空)与电梯非空。当电梯空时,将向上请求与向下请求的数量与距离电梯当前位置综合考虑,选取其中一种同向请求的尽量多的放入电梯的tasks中。当电梯非空时,只接受位移方向与电梯相同且包含于电梯当前楼层与最高tasks的起始楼层之间的请求(并优先捎带近的),保证了顺路的捎带不会影响tasks的接收。如此设计的好处是可以不用一次将请求分配给电梯,先保留在WaitPerson中,由电梯在合适的时候从中获取,容易决定电梯的运行方向并保证了电梯工作量的均衡。缺点是为了避免超载,限制personIn与tasks的和不超过限载人数,影响了电梯的吞吐量。
前两次作业中虽然也有Schedule线程,但由于电梯自己获取请求自己决定方向,于是Schedule所做的事仅是将请求包装一下(主要是为了重写compareTo方法进行排序)丢入WaitPersons中。第三次作业增加了电梯种类,Schedule就参与了设计换乘的事务。首先将WaitPersons容器分为三个,分别装载三个电梯的请求,通过单例模式的getWaitperson传入自己的类型即可获取相应的请求队列。换乘的实现是通过设计一个Person类包装personRequest类,其中定义一个Boolean型变量needChange标识是否需要换乘。Schedule通过对比当前各个WaitPerson的请求数量与当前请求的楼层特性来决定是否需要换乘。如果需要,将needChange置为true并且将请求丢入前一阶段的电梯类型的WaitPersons中,如果不需要就直接分配。电梯在完成请求时会根据needChange是否为true决定是否需要生成一个新person丢回Schedule中。如此设计的好处是保留了电梯自由竞争的框架,减少工作量与修改(偷懒)。缺点是三个WaitPersons容器不流通,调度方法设计不合理容易导致电梯工作量不均匀的情况。
同步块与锁
在我的电梯设计中共享对象主要是电梯的tasks与WaitPersons,虽然InputThread与Schedule也通过共享对象连接,但因为关系非常简单也不会产生错误。WaitPerson需要被Schedule与Elevator调用,于是我将他设计为单例模式并且设置成线程安全类。如此设计保证了WaitPerson的安全性。Elevator中的tasks容器作为共享对象主要是起到线程交互的功能。我将所有对tasks进行修改的工作都集中在Elevator中,所以他并不是真正意义上的共享对象。(事实上外界也不需要获取或者修改电梯的两个容器)。只有当电梯为空且WaitPersons中没有请求可以分配时,电梯会进入tasks.wait状态,当Schedule接收到新的请求时通过tasks.notifyAll唤醒电梯。因此我的同步块也相对较少,设计简单。
同步块主要集中在WaitPerson这个线程安全类中,他的几个主要方法getWaitPersons()(单例模式获取自己),addPerson()(Schedule向Waitpersons中添加请求),getPerEmpty()(电梯空时调用该方法获取乘客),getPass()(电梯非空时调用该方法捎带)都简单粗暴的加上了this锁。其中可以优化的地方是getPass(),因为电梯每次将要移动时都会调用该方法查看是否有可以捎带的请求,是一个先读后根据读的结果决定是否要写的方法,并且很多时候该方法真的只是看看。因此该方法在读的时候可以不需要锁,检测到有合适的请求再加锁去获取。读与写时状态的不完全一致带来些许偏差但影响不大。
调度器设计
由于我采用的分配方法是电梯根据自己状态从WaitPersons中获取合适的请求(不合适的就先放着)并自己决定运行方向,因此调度器就十分清闲。前两次作业都是本着“增加可拓展性”的考虑维持这个线程,只有在第三次作业中才发挥了调度的作用。第三次作业中调度器主要是保证放入每一种电梯WaitPersons中的请求一定是电梯可以接到的,如果需要换乘会置位needChange并且为电梯设置换乘楼层。在Person封装类中写了getDestination方法,如果needChange,则返回换乘楼层,否则返回personRequest中的Destination。至于换乘的实现主要分为两个部分,判断与处理。处理相对简单,由确定的类型转换为另一种确定的类型只需要找到合适的换乘楼层并且设置即可。难点在于如何判断是否需要换乘以及需要什么类型的换乘。在本次试验中我的设计相对简陋,是否需要换乘主要由personRequest的起始楼层与终止楼层的属性,personRequest需要位移的距离,以及各WaitPersons容器的请求数量决定。位移距离远的优先考虑换乘,起始地址或者终止地址在c类电梯中或者接近的考虑换乘到c类电梯...等等。实质上是通过判断指定确定的换乘模式,无法适配与所有情况。WaitPersons容量的考虑结合的该种电梯的数量,但依旧很难处理。因为每获取一个请求就需要决定将他分配到哪个WaitPersons,而无法获知接下来请求的情况。同一个请求在不同的请求群中必然有着不同的最佳处理方式,但由于技术能力不足想不出方法将他们堆积一定数量在处理。换乘只能完全根据当前请求与当前状态实现。
UML图展示

程序bug分析
第一次作业出现了一个非常严重的bug,导致在通过了弱测与中测的情况下强测全军覆没(离谱),原因是当电梯为空的时获取乘客的方法getPerEmpty()获取到第七个请求时判断电梯满了,就把那个人丢掉了...弱测与中测都没有同时进入的超过六个的同向请求,所以没有发现。究其原因还是没有自己造测试数据并且写测试程序。因此第一次作业翻车后马上编写了简单的c语言程序产生数据,并写了java程序分析输出结果是否正确与合法。第二三次作业最后结果上都很不错,强测正确性得到了很好的保证。结合时间输入的大量数据的黑箱测试能发现绝大多数死锁与程序逻辑错误问题,因此第二次作业很顺利的完成。第三次作业主要的问题出在程序结束方面,弱测一个七个请求的点都可能导致程序卡死无法正常结束。因为Elevator可能会产生新的请求,导致Schedule的结束逻辑变得复杂,无法单纯根据InputThread的状态决定是否结束。因此我设计为当Elevator中请求为空时会notify Schedule线程一下,Schedule检测各电梯与WaitPerson的状态决定是否结束程序。但是Schedule检测操作的原子性十分难以保证。而且Schedule结束时向电梯线程发送的notifyAll信号似乎存在各种丢失的可能,导致电梯线程偶尔会无法停止。保守起见我将Schedule与Elevator线程中的Wait()都改成Wait(1000),每一秒起来看看自己是否应该结束了。最终顺利完成了线程的结束,而且对CPU的负载增加没有我想象中那么明显。(毕竟线程进入Wait状态的时候也是少数)。
发现bug策略
非常遗憾此次电梯作业我没有发现别人的bug,我的方法是在别人的代码上修改增加自己的时间输入,然后用生成的1000请求数据去测试,结果大家都能顺利结束并且正常输出,因此就没有做更多尝试。或许可以考虑极端的高并发或者边界数据。
第一单元测试根据输入很快能得到输出,因此很需要构建自动化测试工具来辅助测试,检测结果。而此次作业由于由于可以一次丢入很多请求然后一次处理,感觉就不怎么需要自动化测试的工具,只需要分析输出结果的工具就够了。
心得体会
测试!!测试!! 一定要自己写好测试程序,用充足的数据去测试自己的程序!第五次作业给我带来深刻的教训,我成功骗过了测评机和自己却被强测杀得一塌糊涂(泪目)。逻辑上的可复现的问题用大量数据一测便知,发现问题后就能及时解决。与其浪费时间对着测评机干瞪眼,不如自己编写测试程序生成数据。早写测试程序,早享受!
控制代码的复杂性,思考设计的必要性。
锁只加在共享对象上 ,只有当需要对共享对象修改时才将这一块区域保护,并且需要设置逻辑相关的锁。否则同步块多了有时候不知道自己在保护谁。


浙公网安备 33010602011771号