面向对象的程序设计-模块二课程总结
电梯作业的设计策略
在整体的架构上,我三次作业使用的设计策略较为统一,主要的组成部件有:
- 输入控制线程:用于控制输入,向主等待队列中添加请求(生产者)
- 主控制线程:用于向电梯分发任务,从主等待队列中拿请求(消费者)
- 主线程:主线程使用轮询的方式查看任务是否全部执行完毕,并结束程序
- 电梯线程:用于运行每个电梯的等待队列中的请求
- 主等待队列
WaitList
:一个线程安全的共享对象,设置put
和get
方法来放入、拿走请求,并使用wait()
和notifyAll()
来阻塞或唤醒访问该共享对象的线程
从具体的策略上讲,使用了生产者&消费者模型,输入控制线程和主控制线程构成一对生产者和消费者,二者的共享对象则是主等待队列WaitList
,这里我们加锁来保证同一时间只能有一个线程访问该共享对象,通过wait()
和notify()
来实现阻塞和唤醒访问该共享对象的线程;
主控制线程向电梯线程分配其执行的人员,每一个电梯具有自己的等待队列,而其任务就是循环执行分配到自己的等待队列中的任务,主控制线程和电梯线程之间的共享对象就是每个电梯的等待人员队列,这里我们同样加锁来保证同一时间只能有一个线程访问等待人员队列(共享对象);
-
在单部电梯执行任务的策略上,我使用了LOOK方法,即电梯选定一个方向运送乘客,在执行完当前方向上的所有任务后,再更换方向执行后续任务;
-
在人员分配策略上,我将能够直达的请求按照一定的规则分配给对应电梯进行处理,在这里需要考虑的问题是各个电梯的负载均衡和最终的时间最优。为了将各个因素综合考虑,在这里我设计了一种根据电梯和请求的当前状态进行“打分”的分配方式,即根据
电梯是否已满
、电梯是否正在执行任务
、电梯目前所处楼层和方向和当前任务请求的匹配程度
等几个指标来为每个电梯打分,最终将待分配请求加入得分最高的电梯中(但是由于测试不够&参数没调,导致运行的效果并不好)。对于不能直达的请求,则会先执行前一部分(运到中转层),由电梯线程放入大的等待队列,再由主控制线程进行新一轮的分配。在中转层的选择上,我结合了请求的运行方向
、分配电梯当前的运行方向
和请求发出的楼层
进行了分配。 -
程序结束的判定:输入线程终止且电梯运行完毕。
在具体的判断上,当输入线程读取到null
时,将一个信号量isEnd
置位,主线程通过轮询+Sleep来判断以下条件:- 信号量
isEnd
置位 - 主等待队列为空
- 每个电梯处于空闲状态且每个电梯的等待队列、运行队列均为空
在满足上述的三个条件的情况下结束程序
但是,主线程可能在下述情况下出现错误的判定:
- 最后一个任务从主等待队列中取出,但还没有交给电梯的时候
- 最后一个任务不能直达,从电梯中出来而未放入主等待队列的时候
为了避免这样的情况下程序的提前截止,我让主线程在Sleep一小段时间后再次进行判断,只有两次判断均能满足的时候才会结束运行。
- 信号量
基于度量的程序结构分析
第五次作业:单部傻瓜电梯
- 复杂度分析:
本次作业中,Main由于需要监控各个线程的运行状况并结束程序,且具有一定的逻辑判断深度,所以造成了其OCavg指标复杂度较高;
而类Elevator由于需要维护其两个队列(运行队列和等待队列),并判断和执行相应的任务,单类中执行的功能较多,方法间的互相调用过于复杂,导致其WMC指标复杂度过高。
- 类图:
- 根据SOLID原则进行分析:
- SRP:电梯类Elevator需要执行的功能较多,类内方法互相调用复杂,没有很好地满足单一职责原则;
- OCP:由于电梯的调度策略改变,电梯和调度器均在后续进行了重写
- 其他:本次作业中没有继承和接口,无抽象类
第六次作业:单部可捎带电梯
- 复杂度分析:
在本次作业中,同样是Elevator类设置的功能过多,导致其WMC复杂度较高,Main的逻辑判断深度及复杂度较高😂。 - 类图:本次作业的类图和第五次作业基本相同,不单独列出
- 根据SOLID原则进行分析:
- SRP:同第五次作业,电梯类Elevator需要执行的任务过多;
- OCP:在本次作业中实现了单部电梯的LOOK算法,第七次多电梯作业中的不同属性和捎带策略的电梯是在继承本次电梯的基础上通过拓展实现的
- 其他:本次作业中没有继承和抽象类
第七次作业:多部智能电梯
-
复杂度分析:
- Elevator执行功能过于复杂,Main依然逻辑复杂度很高(
太懒了😭) - Controller类需要监控各个电梯的运行状态,并根据不同的情况决定其运行策略,导致其逻辑判断的深度过高,在调试的过程中,这里也确实出现了很多BUG。
- Elevator执行功能过于复杂,Main依然逻辑复杂度很高(
-
类图:
-
UML时序图分析:
三次作业的时序图基本类似,挑这次作业的画了一下
-
根据SOLID原则进行分析:
- SRP:Elevator需要实现的功能过多,没有很好地遵循单一职责原则;
- OCP:Elevator实现基本的LOOK+捎带策略,ElevatorA/B/C通过继承父类Elevator来修改其各自可以停靠的楼层和不同的属性、捎带策略;
- LSP:父类可以替换子类使用(但是调度策略不同);
- 其他:本次作业中没有实现接口,无抽象类
BUG分析及测试
我的电梯在本阶段作业的公测和互测中没有出现BUG(也没有性能😂),一方面是由于本模块的作业逻辑结构较为清晰,另一方面使用了一些极端样例和大量随机样例进行了测试。对于同组同学的程序,我也没有发现太多的问题。
在测试过程中,我使用了自动化测试来检测正确性,也用它来绘制电梯运行期间的时空图,以监测电梯的性能,统计了电梯的运行轨迹及负载(如下):
电梯运行时空图 图注:
蓝色:A电梯
绿色:B电梯
红色:C电梯
折线:电梯运行轨迹
每个方形的大小:当前电梯内人数
可以看出,在整个电梯运行的过程中,LOOK算法的执行情况较好。但是B电梯的负荷明显过高,而C电梯的任务量太少(运行时间短、运送任务少),电梯整体的运行图能够更加直观地展示在电梯生命周期内的运行负荷,也更易于找出任务分配的不均衡。
在互测阶段,我也为我们组另一位同学画出了这样的运行轨迹图,30S左右输入的40条左右请求,他的电梯运行了160S以上:
从他的电梯运行图中我们也能发现,他的单部电梯在执行任务的过程中使用的算法不是非常合理,例如B电梯出现了大量”折返跑“的情况;多电梯的任务分配也存在问题,C电梯在程序运行期间有长时间”摸鱼“,A电梯亦有空等时间过长的情况。
心得体会
线程安全
- 在设计时考虑到线程之间的同步互斥关系
- 考虑不同线程和共享对象之间的关系
- 合理上锁
- 考虑所有情况,避免不安全的情况出现
设计原则
- 首先保证线程安全
- 在设计时充分考虑以后出现的情况,OCP原则
- 设计清晰每个类的职责,遵循单一职责原则
- 降低类之间的耦合度
- 使用外部工具来进行正确性分析和性能评