BUAA-OO-Unit 2-Summary

BUAA-OO-Unit 2-Summary

一、综述

​ 本单元作业模拟电梯运行系统,考察多线程工程的实现。第一次作业只需实现固定数量纵向电梯的调度与运行;第二次作业增加了横向电梯,同时需要能够根据指令实时增设电梯;第三次作业在此前的基础上迭代增加了横向、纵向电梯交互,需要拆分请求,可能出现换乘的情况。通过三次迭代开发,深入考察了多线程作业的设计要点。

二、同步块和锁

​ 经历了长达一个月的电梯噩梦学习,我深刻体会到同步块设计以及锁的选择是多线程作业保证高效、安全的重中之重。

​ 首先是锁的选择,出于求稳,同时考虑到性能需求,我在三次作业中均只使用了synchronized锁,而没有考虑更复杂的读写锁和条件锁。synchronized锁的优点在于更加省事,同时在发生异常时会释放所有的锁,因此可以避免部分死锁情况的发生。

​ 然后是同步块的设计。我设立了一个RequestQueue类来构造共享对象,只在该类中对既进行读操作又进行写操作的方法进行加锁,保证共享对象的安全性。此外,在其它类中需要循环遍历共享对象且共享对象会发生改变时,两个线程同时访问一个共享对象,可能由于对象中属性发生变化而产生各种麻烦的问题,因此先给共享对象加锁,可以一劳永逸地解决一切潜在的问题。

三、调度器设计

​ 从第一次作业开始我就设计了专门的调度器schedule类,尽管在前两次作业中并不是特别必要,但是在第三次作业的迭代开发中专门的调度器就十分重要了。

​ 首先,我采取的是自由竞争模式,每个楼座、楼层都有一个专门的共享队列存放请求。因此,调度器需要完成的任务是:从总的请求队列中拿出一个请求,根据该请求的起始与终止位置将该请求放入指定楼座或楼层的共享队列中,然后从总请求队列中删除该请求。

​ 在第三次作业比较特殊,由于我将每一个请求作为一个大请求,其拆分出的小请求作为大请求类中的一个属性,因此schedule还需要读出当前大请求中的第一个小请求,再进行上述操作,将大请求放入指定楼座或楼层的共享队列中,然后从总请求队列中删除该请求;同时,由于一次只能完成一个小请求,因此需要在电梯运行类中,额外判断该大请求是否还含有小请求,如果有的话需要再将它放入总请求队列中,让调度器再次进行调度。

四、架构设计

  • 第一次作业

    • uml类图

    • 设计架构:

      使用生产者-消费者模型。

      1. 分别开设输入线程、五部电梯线程、调度器线程。
      2. 输入线程InputThread读入一个请求,将存储进总的请求队列waitQueue中。
      3. 每一座设置一个专门的待处理请求队列,终止条件达成前,调度器线程Schedule将不停从waitQueue中的请求分配到对应的待处理队列中,电梯线程则将不停处理待处理队列中的请求。其中,电梯的运行策略在单独的策略类Strategy中完成,提高了可拓展性。
      4. 输出线程OutputThread输出信息。
    • 电梯策略:

      采用Look策略:

      1. 当电梯有乘客时,以乘客中目标楼层距离当前楼层最远的请求为主请求确定目标楼层
      2. 当电梯没有乘客时,首先按照原方向,寻找距离当前楼层最远的有请求的楼层,找到则确定为目标楼层,这次寻找最终结果有可能会是当前层。如果寻找无果(沿方向的所有层包括本层都没有请求)则改变方向,继续寻找。若最后没有找到,则可以返回一个标志结束的值表示电梯进入空闲。
  • 第二次作业

    • uml类图

    • 设计架构

      总体架构与第一次一致。改动如下:

      1. 增设了横向电梯类与横向电梯策略类。
      2. 为满足能在运行过程中增设电梯的需求,在InputThread中识别到增设电梯的指令后即增设一部电梯。
      3. 对于同一楼层或楼座的多部电梯,依然仅设立一个对应于楼座或楼层的待处理队列,采取自由竞争策略。在电梯类与策略类中额外给对象加锁,防止共享对象不安全的情况。
    • 电梯策略

      纵向电梯策略不变。对于横向电梯,同样使用Look策略。值得一提的是横向请求方向可以是双向的,因此要通过目的楼座与起始楼座的距离distance = (toBuilding - fromBuilding + 5) % 5来进行判断,当大于等于3时,逆时针方向;小于三时,顺时针方向。

  • 第三次作业

    • uml类图

    • UML协作图

    • 设计架构

      • 总体架构依然保持不变。由于本次请求可能会出现换乘的情况,改动如下:

        1. 新开设TotalPassengerRequest类。
        2. InputThread判断请求是否需要换乘。其中,根据掩码判断是否能在该楼层换乘。
        3. 对于一个大请求,若需要换乘则将原本请求拆分为多个小请求,存入TotalPassengerRequest类的容器中。
        4. 在调度器中,根据大请求中第一个小请求将大请求分配进相应待处理队列。在电梯类中,处理完大请求中的第一个小请求后,需要判断该大请求是否还含有未处理的小请求,若有,则要再次把该大请求放入waitQueue中,由Schedule进行调度。
        5. 需要改动Schedule类中判断调度终止的条件,否则会出现无法结束线程的问题。解决方法是:在waitQueue中设立numTodonumFinish两个属性,有请求放入waitQueuenumTodo加一,处理完一个大请求时numFinish加一,当numTodo等于numFinish时,调度器才会终止调度。
      • 图示如下:

    • 电梯策略

      与第二次作业保持一致。

五、bug分析

  • 第一次作业:
    • 电梯运行策略出现纰漏,轿厢内没有乘客时,只会接上等待队列中符合进电梯条件的一个人,导致运行时间太长,强测中爆了几个RTLE的点。
    • 没有考虑输出线程安全问题,互测中被hack出了时间戳不递增的问题。
  • 第二次作业:
    • 由于对waitnotify理解欠深刻,在共享对象类的每个方法中都加了notifyall()方法,导致轮询。
    • 没有解决好共享对象线程不安全的问题,导致会出现诸如野指针、死锁等各种奇怪的bug。
    • 以上两个bug都足以致命,也并不是很隐蔽,但是当时懒虫上身,没有做足够的本地测试,最后强测只过了两个点,可以说是大寄特寄了。
  • 第三次作业:
    • 痛定思痛,吸收了第二次作业的教训,进行了充分的本地测试,强测互测均未出现bug。

六、hack策略

​ 针对轮询,首先可以通过idea自带的插件与查看任务管理器中cpu使用情况的方法来判断是否轮询。接下来可以在特定位置用println输出信息,如果程序运行时某一处重复输出该信息,即可知道哪里发生了轮询。

​ 对于死锁的问题,可以通过调试的方法来进行检查。

​ 本单元我主要还是根据自己本地测试中出现的bug来构造hack数据。结果表明这样的测试方法还是比较有效的,因为诸如轮询、死锁等问题是比较普遍的。与第一单元相比,随机数据的hack成功率大大降低,白盒测试或是根据已有bug测试在本单元中是更优的方法。

七、心得体会

  • SOLID原则

    ​ 基于单一责任原则,尽可能使得每个类和方法负责单一功能,比如开设专门的调度器类与电梯运行的策略 类。这样的设计大大增强了代码的可读性和可拓展性。

  • 层次化设计

    ​ 由于较好遵循了SOLID原则,有着读入请求、拆分请求、调度请求、电梯策略、电梯运行、输出结果这样明确的逻辑顺序,同时各自设置了一个类来完成任务,代码的层次结构是比较清晰的。好的层次化设计提供了高可用性和高扩展性,在构建工程时需要优先考虑。

  • 线程安全

    • 单例模式:第一次作业出现的输出线程不安全问题让我彻底重视起单例模式的使用。单例模式确保了一个类只有一个实例,非常适合多线程线程池需要频繁实例化,创建的对象又频繁被更替、销毁的特性。
    • :锁是保护线程安全最重要的部分,使用synchronized关键字,可以确保该对象只被一个进程调用,避免多个进程同时调用同一个对象中的一个属性甚至死锁的问题。
    • 合理设置wait()notifyAll()只在必要时进行notifyAll,可以有效避免轮询的问题。
    • 线程安全容器:我使用了CopyOnWriteArrayList作为共享对象类中存储请求队列的容器,使用该容器可以有效避免读写冲突导致的线程不安全,同时也可以实现一边遍历容器,一边对容器进行更改,而无需使用迭代器,非常实用。
  • 总结

    ​ 通过本次作业,收获了很多宝贵的经验,比如:如何合适的选择锁、在什么时候要加锁、什么是观察者模式、什么又是生产者-消费者模式等等。

    ​ 同时,本单元我没有进行一次重构,迭代过程中中几乎只需要完成增量开发;代码解耦性和可读性也相比第一单元作业有了很大的提升。这也让我感到自身确实有了一些实实在在的进步。

    ​ 电梯单元是我第一次接触到多线程,一开始确实感到难度很大,不知道如何构建设计架构。但是随着一次次作业的迭代,不管是构建思路还是操刀完成代码的过程都越来越顺畅,对多线程的理解也越来越深入。因此本单元的体验感和收获感是很强的。

  • 建议

    ​ 目前的中测与强测强度差距似乎有些过大,一个漏洞百出的程序都能通过中测,尽管这样能降低有效作业的门槛,也能鼓励同学们更多地进行本地测试,但是个人觉得如果课程组能适当加强中测的强度的话会更有利同学更好完成任务。

posted @ 2022-05-03 22:31  yeger118  阅读(27)  评论(2编辑  收藏  举报