OO第二单元总结
OO unit2
需求迭代
第一次作业
-
输入:只有乘客的搭乘请求,搭乘请求为纵向
-
一共有五台纵向电梯,分别对应五个座
ABCDE
第二次作业
-
输入:乘客的搭乘请求,搭乘请求为纵向或者横向;电梯的增加请求
-
初始有五台纵向电梯
第三次作业
-
输入:乘客的搭乘请求,搭乘请求为纵向或者横向或者任意;电梯的增加请求
-
初始有五台纵向电梯和一台一楼的横向电梯
-
电梯的可达性有限制,电梯的运行速度参数和载量参数可指定
总体架构与思路
三次作业的整体实现思路都是使用了生产者-消费者模型。标准输入不断读入请求,放入共享对象缓存队列,这是生产者;所有电梯取出并完成队列中的请求(完成的过程就是将乘客送往目的地的过程),这是消费者。
三次作业的整体架构,我都参考了第三次课上实验的生产者消费者的整体架构,通过isEnd信号来依次结束输入进程、调度进程和清空了对应缓存队列的电梯进程,使用wait-Notify的方式进行编程。
第一次作业
锁与同步块
-
针对共享对象请求队列的方法和mainReq的获得过程设置了同步块,将涉及到对共享对象读写的方法都进行了加锁
-
反思:加锁的范围太大——第一次作业中多部电梯在各个楼栋间互不干涉,同时共享对象“请求队列”结构floorQueue是分楼栋设置的,实际上并不会造成线程安全问题,滥用锁机制导致了程序性能下降
调度器设计
-
将调度器设计为中转线程
-
通过调度器线程实现:输入线程得到请求后立即将其放入对应楼座的请求队列,电梯线程再从对应楼座的请求队列中获取请求
电梯策略及算法
-
依照指导书的基准策略,使用了ALS的调度策略
-
ALS调度算法:该算法的核心在于为电梯类建立一个求得主请求mainRequest的方法;每层调用该方法、根据ALS的要求更新得到主请求,从而指导电梯接下来的运行方向
架构
第二次作业
锁与同步块
-
与第一次作业大致相同
-
同类电梯(e.g.同在5楼的横向电梯,同在一个building内的纵向电梯)访问的对象——floorQueue和BuildingQueue的加锁
调度器设计
-
与第一次作业大致相同
-
处理新增电梯请求时,将电梯放入同类电梯(e.g.同在5楼的横向电梯,同在一个building内的纵向电梯)类EleSame进行统一管理,并且开启电梯线程
策略及算法
电梯策略
-
横向、纵向电梯分别采用ALS的运载策略,通过重写mainReq的获得方法来实现纵向向横向的扩展。
调度算法
-
采用自由竞争的调度算法,每次调度器仅将请求加入各楼座(层)的请求队列,由本座(层)的所有电梯自由竞争这些请求
-
优点:实现简单,代码复杂度较低
-
缺点:当请求较少时多部电梯会一起冲向同一个请求直至该请求被完成,造成电梯资源的浪费
-
架构
第三次作业
最后一次作业为无效作业,没有实现第三次作业的迭代开发需求,只是完成了第一二次作业的回归测试,经检查后发现是在调用“需要换乘类请求”队列中元素并删除时与非换成类请求的调用与删除的操作出现了问题。反思一下后认为,不应该简单粗暴地增加一个换乘类,而是应该在原有的请求类上进行进一步封装,用面向对象编程的思维来解决问题。
架构
用新的NewPersonReq类将PersonRequset类封装起来,存储换成信息
UML协作图
bug分析
几次作业的主要问题都是线程安全类的问题和轮询的问题(同时,在hack其他同学的时候,我也主要从这两个出发点来随机测试一些数据)
线程安全
①官方输出包并不安全。第一次评测受到教训后,我借鉴讨论区林世瀚同学(http://oo.buaa.edu.cn/assignment/332/discussion/1148)的方法,通过static关键字的巧妙使用,使得输出实现了线程安全。
②电梯进程中某一个会访问共享请求对象的方法忘记了加锁。线程安全的主要问题就是对所有的共享对象(涉及两个以上的线程对其进行读写)的操作进行上锁即可。
轮询
在每个线程的run()
方法中,如果一次while循环并没有做什么有意义的事情、而只是没有进入条件分支空跑,就会导致轮询。
借鉴了匡莉同学在讨论区提出的de轮询bug的方法(http://oo.buaa.edu.cn/assignment/332/discussion/1161):
①在不同线程里的while循环开头加入标志输出,初步定为轮询问题的位置
②使用合适的测试数据,当运行该数据的过程中出现轮询问题之后(重复①中的输出),进一步进行调试。
③使用IDEA的'Windows Async Profiler'来分析进程CPU占比,如果有某一个函数占比特别大,那么这个函数出问题的概率也特别大
我的程序的bug就主要出现在getMainReq()获取ALS调度方法中的主请求这个过程中:当上一轮的主请求没有完成时,直接保留当前mainReq,造成了这个函数空跑很多次。
心得体会
①线程安全:使用线程安全类之后,程序的线程安全性就可以说是有了一半保证。通过锁的方法保证互斥, 通过资源保证互斥(同时完成了线程间的协作通信),或者选用java自带的线程安全的容器(比如BlockingQueue),这些都是很好的许纳泽。同时,临界区的选择也要恰当,要使其在保证线程安全的前提下下尽量小。
②设计模式:第一第二次作业,通过清晰的生产者消费者模型和工厂模式建立新电梯,实现了比较轻松的迭代和增量开发。而第三次作业,我在复用上一次的代码和推翻使用新的设计模式来实现进程间协作之间摇摆不定,最后造成了第三次作业的失败。开始我曾以为是生产者消费者模式就是没有办法解决进程之间交互的问题,后来跟朋友交流之后才明白知识因为我对该模型理解不够透彻,才会找不到合适的切入点。希望以后可以对设计模式有更多的更深层次的了解和使用使用。
③层次化设计:各个代码板块的职能要尽可能的分开,避免耦合,这样才能减少debug的难度,在bug出现之后才可以定位到具体的地方。各个职能不通的进程就更应该分开了,这次作业中,将输入进程单独列为一个进程,而不是和mainclass耦合在一起,这就是一个很好的例子。