OO第二单元总结

OO第二单元总结

摘要

  • 第一次作业:基本目标是模拟多线程实时电梯系统,模拟多部同型号电梯的运行,熟悉线程的创建、运行等基本操作,熟悉多线程的设计方法;

  • 第二次作业:在第一次作业的基础上,掌握线程安全知识并解决线程安全问题,同时在架构上围绕线程之间的协同设计层次架构,模拟一个多线程环形实时电梯系统

  • 第三次作业:在前两次作业的基础上,掌握线程之间的交互,强化线程之间的协同设计层次架构,且可以动态增加电梯电梯属性可定制

本单元的作业主要是多线程相关,同时题目有了更加实际具体的电梯背景,编写代码时,更加注重代码的工程规范和架构设计。三周的趋势大体由难道易,从第一次作业的畏手畏脚,到第二次作业的顺利完成,再到第三次作业的大意失荆州,体会万分。

 

第一次作业

设计策略

以生产者-消费者模型为基础,存在两对生产者和消费者,即输入线程作为生产者、调度器作为消费者从输入线程中获取数据同时又作为生产者给电梯提供数据、电梯作为消费者。设计了调度器Scheduler类,大致流程是MainClass启动生产者线程InputProducer、调度器Scheduler线程、消费者Elevator线程,最后由InputProducer线程向Scheduler、Elevator线程逐级setEnd结束线程。

调度器设计

在调度策略上采用“Look算法”,模仿日常生活中的电梯调度策略,即电梯在1-10楼之间来回扫描,在期间捎带乘客;当某一时刻电梯中乘客的方向都沿电梯的反方向时且电梯运行方向上没有新的请求时,立即转向;当电梯上没有人时,电梯在原地。输入线程负责将输入放到等待队列,调度器从等待队列拿取请求,之后分给对应楼座的电梯,若等待队列为空,wait在等待队列上等待输入线程唤醒。调度器中一个ArrayList<Elevator> processingQueue存储电梯。

架构分析

Elevator线程负责完成上行下行、接人放人等操作并输出相关信息,其中有一个ArrayList<PersonRequest> RequestTable作为电梯候乘表,一个ArrayList<PersonRequest> Passengers 作为电梯中的乘客表Scheduler将请求放入RequestTable,之后通过look算法从RequestTable中获取请求进入Passengers中 。

  • UML协作图

  • UML图

     

同步块设置和锁的选择

本次作业仅有RequestsQueue为线程安全类,使用synchronized加锁。线程安全类RequestsQueue中维护了requests的ArrayList。此外在Elevator线程中设置了sychronized锁,锁的对象是电梯候乘表,等Passengers和RequestTable为空且没有end时wait,通过调度器放入请求时来唤醒Elevator。

第一次作业是自己初次进行多线程相关的代码编写,通过理论课,加上自行搜索资料阅读,在实验课的训练后对多线程也有了大致的初印象。第一次作业的架构比较简单,编写代码过程中耗时的地方多是对电梯运行逻辑的思考,自己作为新手比较谨慎,编码过程中对线程的处理格外小心,所以并没有遇到诸如轮询或者死锁等问题。

Bug分析

本次作业在中测和强测中未出现Bug但在互测中出现了一个Bug,Bug出现的原因是忽略了官方包的线程不安全,导致输出的时间可能出现不是递增的情况,同一互测房内的同学也都是和我一样的原因被Hack。通过设置线程安全类SafeOutput,其中对调用官方包的输出方法进行synchronized statics包装,上锁对象是此类本身解决,之后两次作业此类一直沿用。

发现别人Bug的策略

本次采用了手动构造测试数据的策略,但是尝试构造了极端数据,仍未成功。

 

第二次作业

较前次作业新增的点

  1. 增加可以横向移动的电梯;

  2. 增加新功能:电梯可以动态添加;(request增加elevatorRequest);

设计策略

  • 纵向电梯 沿用第五次作业的设计

  • 横向电梯 仿照纵向电梯的逻辑,因为二者互不影响,所以可以仿照编写。

    循环运转,运行逻辑和纵向电梯稍有不同。

  • 电梯可以动态增加

修改调度器scheduler,进行电梯调度和电梯的动态增加。电梯的存储改用两个二维ArrayList分别存储所有横向电梯(ArrayList<ArrayList<Rounder>> rounders)和纵向电梯(ArrayList<ArrayList<Elevator>> >elevators)。此外所有访问这两个二维ArrayList的地方都用sychronized上锁。

调度器设计

调度器在上一次作业的基础上增加一个将新增呢电梯添加进对应二维ArrayList中的方法。在调度策略上,主要考虑过以下几种:

  • 随机分配

    利用Random类生成随机数,对应电梯编号进行随机调度。

  • 按电梯加入顺序依次分配

    均匀分配,不一定是最佳,但是对于随机数据较好。

  • 多电梯争抢

    未争抢到的电梯需要额外地移动,但是电梯在什么楼层或楼座,对于随机输入的数据没有明显的优劣,对实际速度影响不大。

  • 根据电梯的人数分配,人数少的优先

    尽量让电梯的人数都均匀,少出现电梯满员有人等待的情况。

最后我选择了随机调度(因为写起来简单

架构分析

与上一次作业基本一致,只是添加了Rounder类作为新增的横向电梯类

  • UML图

     

  • UML协作图

  

 

 

同步块设置和锁的选择

synchronized锁和读写锁之间做过权衡,最后还是选择synchronized

线程安全类是RequestTable , 上锁的地方和第一次作业一致,增加上锁地方是Rounder(横向电梯类)中没有请求需要执行时wait,以及调度器处理增加电梯的方法时对所要修改的二维数组上锁。

Bug分析

本次作业在中测和强测中未出现Bug,在互测中有一个Bug,仔细检查后推测是由于随机调度时多次将请求调度到了同一台电梯里导致TLE,重新提交一次后修复了Bug(这也是随机调度高不确定性的缺点)

发现别人Bug的策略

本次仍采用了手动构造测试数据的策略,依然未成功。互测房中有同学生成了大量随机数据,似乎干倒了几位同学,我想可能是线程不安全造成的

 

第三次作业

较前两次作业的新增需求

  1. 增加换乘机制

  2. 新增电梯定制化功能,可自定义电梯容量,运行速度

  3. 横向电梯新增可开门楼座的约束

  4. 初始电梯数目和种类改变

设计策略

应对新增需求的方案:

  1. 对于换乘机制的应对:使用步骤的队列,增加请求时每次向队列中增加一个请求,电梯从队列中拿出小的请求并处理。若整个请求没有处理完,则向队列中添加下一次小请求,再放回请求输入。新增类Person,InputProducer对每个输入的PersonRequest均构造一个Person,每一个PersonRequeset对应一个Person,person中除了又PersonRequest类的属性,还新增属性curDestFloor和curDestBuilding分别记录当前PersonRequest要去的楼层和楼座,调度器根据CurDestFloor和curDestBuilding来调度每一个需求,当curDestFloor == toFloor && curDestBuilding == toBuilding 时代表该需求完成,否则需要重新加入waitingQueu中,接受下一次的调度。

  2. 对于电梯看定制化:添加对应的新属性配置Getter、Setter方法和新增构造器即可;

  3. 横向电梯的可开门楼座约束添加变量M存储,当到达一个新楼座时根据M判断是否可以开门;

  4. 对电梯线程的初始化数目和构造器进行轻微改动即可。

调度器设计

调度器中的成员和大致结构同前两次作业。不同点是修改了调度器将人的请求送入某个电梯的方法,同时不再使用随机调度的方案,改为根据电梯的人数分配,人数少的优先的方式,尽量让电梯的人数都均匀,少出现电梯满员有人等待的情况。

架构分析

  • UML类图

  • UML协作图

 

本次作业在结束线程时与前两次作业不同。本次在InputProducer类中增加一个ArrayList<Person> Persons 存储所有的人的需求,若接收到NULL输入,inputProducer先判断所有人是否都到达目的地,若都到达才setEnd,否则进行wait,此外将PersonRequest类重新封装乘Person类,Person类中增加布尔类型成员判断是否到达目的地,此值默认FALSE,当某个人到达目的地由该电梯将FALSE改为TRUE,并notifyAllinputProducer进行重新便利所有人请求。当inputProducer进行setEnd操作后,Scheduler和电梯线程依次结束。

同步块设置和锁的选择

继续使用synchronized的方法上锁,保持前两次作业上锁的地方不变,增加上锁的地方有InputProducer遍历的ArrayList<Person> Persons时对它上锁,以及电梯线程将人送到目的地后对person修改布尔值为真后唤醒InputProducer的地方上锁。

每次电梯将人送到curDest时都要唤醒等待队列,防止出现有人到达中转地需要再次被调度时却未被放入等待队列的情况。

Bug分析

本次作业在强测中出现了较严重的Bug,Bug出现的原因有两类:一是未初始化Person类中的curDest属性,导致可能出现人的请求会是0层,从而该人无法被送达,线程永远无法结束,而出现TLE错误;二是调度策略上虽然未选择随机调度,但是仅仅通过判断电梯的载客量来调度电梯的方式,未综合考虑其它因素,仍然可能导致某个电梯候乘人数过多而出现超时送达导致的TLE问题。对于第一类Bug解决方式是调度器调度时顺带设置CurDestFloor与curDestBuilding,对于第二类Bug只能大改调度策略,将电梯候乘人数,运行速度,距离要接人的距离等因素综合考虑,自己未能较好的修复。

发现别人Bug的策略

本次作业尝试构造极端数据,强测后进入了C房,Hack房友的战绩是:(11/12)达到了91%的Hack成功率,房友的问题大多都是中转楼座到达后未能较好地将该请求再次进行调度导致的逻辑错误,也有同学犯了和我一样没有正确设置中转楼层的错误;还有同学忘记考虑横向电梯的可停靠楼座,导致在每一个楼座都停靠而出错。

 

代码架构可扩展性分析

本单元三次作业一直沿用两级“生产者消费者”的架构进行多线程设计,从第一次到第二次作业的扩展较轻松,只需简单修改调度器和添加与纵向电梯基本一样的横向电梯类即可完成,但由第二次作业向第三次作业的迭代并不轻松,在将某个请求送到中转点后需要将该请求再次放入等待队列中再次调度这一点上需要多个线程同时协作,且到第三次作业时由于之前都在用synchronized上锁,导致除了线程安全类RequestTable外的其他类中也累积了许多synchronize块,甚至一些地方出现了锁的嵌套,为了防止出现死锁,在上锁时小心翼翼,还进行了一些浪费性能保证线程逻辑正确的操作。因此我的第二次作业到第三次作业的迭代并不成功。

因此,我认为第三次作业的迭代应该模仿第二次实验代码的写法对架构进行修改:

  • 专门的线程调度器线程

    处理较为复杂,要考虑线程安全问题

  • 控制器

    提供单例模式,只提供一些公共方法,而不占用线程。所有的请求进入和处理都通过控制器的方法实现,便于实现

调度策略方面也应该综合考虑各项因素,或许应该建立一个方法类来辅助调度器的决策。

SOLID设计原则中有一点是:依赖倒置原则:即各类相互依赖情况较少,利于拓展。自己的代码中未较好体现这一点,有许多线程之间共享的成员变量,造成各类虽然分工明确但依然存在较大耦合度,导致第三次作业的迭代不成功。

 

心得感想

关于线程安全

线程安全是多线程单元永恒的命题。不考虑清楚,每一步都战战兢兢。

虽然本次代码上没有出现过严重的线程安全问题,但是自己在加锁上缺乏节制,三次作业累加起来代码中存在十分多的互斥锁,如果还有第四次作业一定需要重构了。关于避免轮询,就是按部就班照着教程理解后处理的,对于每一个wait的地方都考虑清楚由谁Notify再写,因此也未出现CTLE问题。

线程安全应该在设计阶段就应该考虑清楚,由正确的架构来保证,而非面向过程式地添加互斥锁。

关于层次化设计

整个单元“生产者-消费者模式”贯穿其中,也是一个学以致用的实践过程。

本单元的架构我的思路是让输出入模块调度器模块和电梯模块尽可能分工明确,为了满足单一责任原则,我设计时将生产者线程只负责读入请求,并将其加入请求队列或添加电梯;调取器仅负责将请求队列中的请求分配给电梯各自的队列;消费者线程负责处理请求。三次作业均使用第一次作业设计的电梯,基本无需改动。生产者线程只是根据题目的要求进行了小的修改,之后的扩展也只需要修改调度器,满足了开放封闭原则。不足点就是死板地使用“生产-消费者”模式,线程之间共享的成员变量太多,忽略了依赖倒置原则

由此可见,多线程的设计较之单一线程要实现合理的层次化不仅要将各类的分工设计明确,还要想办法减少各类之间的相互依赖,比如多构造一些类来细化解耦某一工作。

其他

本单元在测试上做的仍然不够充分,也有面向测评机的嫌疑,因此也在第三次作业的强测中吃大亏,下一单元需要在测试上花费更多时间,不能被中测通过所迷惑。第二单元完结撒花!

 

posted @ 2022-05-02 17:46  BruceHimself  阅读(71)  评论(1编辑  收藏  举报