• BUAA OO Unit2 Summary


    BUAA OO Unit2 Summary

    一. 设计策略

      早在大一的时候,我就对OO的电梯作业略有耳闻,以至于在等电梯等时候总会偷偷地思考:电梯到底是怎样调度的呢?而在这一个单元,我真正亲身体验到了多线程和电梯调度的玄妙之处。

      我们这三次作业的电梯,是一种比较特殊的电梯——目的选层电梯。乘客在电梯外,输出自己的目的楼层,作为一条请求输入电梯系统。我们需要根据输入的请求的情况,来进行电梯调度。

    设计模式

      这三次作业中,我都使用了消费者-生产者模式。

    • 输出线程:作为生产者,从控制台读取请求之后把请求put到调度器中的请求队列中。
    • 电梯线程:作为消费者,从调度器的请求队列中获取请求,并根据自身的运行逻辑执行请求。
    • 调度器:作为托盘,管理所有的共享变量。采用单例模式,生产者和消费者调用调度器实例的方法访问共享变量,调度器使用synchronized关键字保证共享变量访问的原子性。

    电梯捎带策略

      在三次作业中,电梯捎带策略我都采用了LOOK算法,简单来说就是:

    • 电梯不区分主次请求。
    • 电梯会沿着当前的方向一直行驶直到需要转向。
    • 电梯每到一层与电梯外的请求队列进行交互,在条件允许的情况下捎带所有同方向的请求。

      而对于电梯需要转向的条件如下:

    • 电梯已行至顶楼或者是底层
    • 电梯中的没有乘客的目的楼层在当前方向上
    • 当前方向上的楼层没有请求,或者有请求但是电梯已经满载

    请求分配策略

      第六、七次作业涉及到多电梯调度,所以存在请求分配策略的问题。

      第六次作业

      第六次作业中,我并没有采用调度器分配的策略,而是任由电梯自行“抢人”。当请求到来的时候,通知所有的电梯,先到达出发楼层的电梯就会先获得该请求,并从调度器的请求队列中删除该项请求。

      这样调度的好处是,能够保证每一个请求都能尽快地进入电梯,从而整体的运行时间能够缩短。虽然会导致所有电梯扎堆的情况,但是对于性能的影响不是很大。

      针对电梯“扎堆”的现象,我做了一点小小的优化,就是电梯在初始和空闲的时候,默认停靠在不同的楼层,能够在一定程度上分散电梯,在一些情况下可以优化电梯的性能。

      第七次作业

      在第七次作业中,因为涉及了电梯的换乘问题,所以第七次作业中我采用了调度器分配请求给电梯的策略。

      考虑到A电梯速度最快并且-3和15~20楼只有A电梯可达;B电梯速度适中,容量大,适用范围最广;C电梯虽然速度最慢,但是涉及一个特殊的楼层——3楼。所以请求分配采用以下策略:

    • 所有目的楼层在-3~1和15~20楼的请求优先分给A电梯
    • 中间楼层(2~14楼)层中奇数楼层到奇数楼层的请求优先分给C电梯
    • 剩下的请求全部划给B电梯

      这样的分配策略虽然不是最优的分配策略,但是能够保证请求能够被平均地分配给各个电梯,不会出现一些电梯饥饿,一些电梯繁忙的现象,整体上来说性能不算优越但是还算能看。

      另外因为部分楼层只有一定的电梯才可达,所以需要进行请求拆分:

    • 设置中转楼层:1楼和15楼
    • 中间楼层去往-3~-1和16-20楼的请求需要进行拆分
    • 3楼去往偶数楼层或者偶数楼层去往3楼的请求需要进行拆分
    • 在调度器中有一个请求缓存队列,拆分后的第二条请求暂存在缓存队列中,等待第一条请求执行完毕后,电梯会调调度器的方法,在将第二条请求从缓存队列中取出,按上述分配规则分配给电梯

    设计策略的不足

      在作业结束以后分析和反思自己的设计策略,发现有如下的不足之处:

    • 在第七次作业中,因为涉及到将请求分配给电梯到操作,所以调度器需要知道电梯的运行状态。我采用的方式是在调度器中调用电梯的方法来获取电梯的运行状态,这是一种非常不好的方法,在一个线程中调用另外一个线程的方法,极有可能导致线程安全问题。在课上老师介绍了status board的方法,电梯线程主动把状态发布到status board之上,调度器从status board来读取电梯状态,要比在调度器中调用电梯的方法要好很多。
    • 另外,在换乘电梯的时候,我是在第一步请求执行结束之后,才会把第二步请求分配给接任的电梯,这样在人从第一步电梯下来之后还需要等待第二部电梯到达换乘楼层,这样就会浪费很多时间,造成性能不是很好。正确的做法应该是在第一步请求执行的过程中,如果接任的电梯空闲的话,应该提前到达换乘楼层等待。

    二. 量度分析

    2.1 第一次作业代码结构分析

      UML类图

      UML时序图

      代码复杂度分析

      可以看到,调度器和电梯类的平均圈复杂度和总圈复杂度较高。

      调度器中复杂度较高的方法是一个电梯检查楼层中是否有新需求以及得到新需求所在的楼层的方法,这个方法涉及大量的if_else结构,有很多分支和操作其实是冗余的,可以做一些优化。

      电梯中复杂度较高的方法是判断是否需要转向的方法和决定下一步行动的方法(是否需要休眠以及休眠结束之后需要采取的行动),这两个方法耦合度较高,而且同样涉及大量的条件分支。遗憾的是,目前我暂时还没能找到一个有效的方法来优化这两个函数。

    2.2 第二次作业代码结构分析

      UML类图

      UML时序图

      复杂度分析

      第二次作业我基本上沿用了第一次作业的设计,所以整体上没有什么大的变化,方法复杂度情况也基本上与第一次作业情况类似,依旧是几个涉及大量条件判断的方法复杂度较高。

    2.3 第三次作业代码结构分析

      UML类图

      UML时序图

      复杂度分析

      因为第三次作业采用了和前两次作业不同的分配请求的方法,所以代码的整体结构和前两次有了比较大的不同。

      因为把电梯的局部请求队列放在了电梯类里,所以我第三次作业的电梯类非常地庞大和冗杂,这显然不是一个很好的代码结构设计。一个比较好的代码结构应该是把局部请求队列独立于电梯线程,甚至能把电梯的调度算法抽象出一个子调度器。

      在全局调度器中,因为涉及到拆分指令和分配请求,二者都涉及到大量的条件判断,所以相关方法的复杂度就比较高,目前还没有有效的解决办法。

      值得一提的是我的电梯工厂的代码非结构化程度比较高,主要是因为这次作业只涉及三种电梯的建造,所以我就只写了一个简单工厂模式。显然这样的设计程序的结构化和可扩展性很差,不利于迭代开发,属于个人的偷懒行为,实际的程序开发中并不可取。

    2.4 SOLID原则分析

    SRP 单一责任原则

      我的设计中输入线程只负责处理控制台的输入和提交请求,电梯线程仅负责自身的运行和执行请求,总调度器负责接收请求,处理请求和将请求分配费电梯,基本上还是符合SRP原则。但是设计上还能更加优化一些,比如把电梯的调度算法单独抽象出来,电梯类仅负责开关门和上下人,也能更加方便后续的优化。

    OCP开放封闭原则

      三次作业中电梯运行算法基本上是沿用第一次作业的,仅是在随着指导书做了一些限制条件的修改,所以电梯运行基本上做到OCP原则。

      调度器第一、二次作业基本上没有修改,但是第三次作业做了一个较大的修改。尤其是第三次作业为每个电梯设置了局部请求队列,并且把局部请求队列并入电梯类。当时仅仅只是为了一些操作方便就采取了这样的方式,但是这不是一个好的设计方式,打破了原有的设计结构。

    LSP里氏替换原则

      三次作业都未涉及继承。

    ISP 接口分离原则

      仅存在线程继承了Runnable接口。

    DIP 依赖倒置原则

      在第七次作业中,电梯类是一个抽象的结构,三种电梯都依赖于相同的电梯类,符合DIP原则。

    三. TEST & BUGS

    3.1 分析自己程序的BUG

      三次作业的强测和互测中,在第一次作业的强测中发现了一个bug,是由于在电梯线程被唤醒之后,判断线程是否需要结束的条件未加入“请求队列和电梯内乘客队列应都为空”这一条件所致,所以在一个强测点中出现了漏了最后一条请求,电梯线程提前结束的bug。

    3.2 发现别人程序的BUG

      在互测中采用的策略依旧是自动测评机覆盖性测试和少量极端数据定点爆破的方式。

      在这次作业中,大家的Bug明显少了很多,经历了第一单元的腥风血雨,大家程序的防御性都强了不少。找到的仅有几处bug也是因为线程安全造成的bug,在此处就不赘述了。

    四. 心得体会

    • 第一次接触多线程。这一单元的作业是我第一次接触到了多线程编程,在刚刚上完课的时候,我对于多线程还是属于一头雾水的状态,不理解线程同步,不理解锁的实质,在编写代码出bug的时候,也不知道该如何调试多线程程序。在经历了三次迭代作业训练,以及OS课程中关于信号量机制的学习之后,逐渐对多线程有了一些初步的认识。
    • 线程安全很重要。这个单元最折磨人的就是线程安全问题。一方面,有些线程安全BUG的出现条件十分玄学,本地的测评机有时候也没办法测出来;另一方面,线程安全问题在本地复现也比较困难,也给debug造成了一定的困难。一旦出现了死锁问题,如何优雅地解决死锁也是一大难题,有时候甚至需要把整个架构推到重来。所以最好的方式就是在思考设计的时候,随时都考虑到线程安全问题,从源头上避免死锁的出现。
    • 留有遗憾的一单元。其实这一单元的作业,真的有很多东西值得我们去思考。在作业结束之后一周的研讨课上,以及平时的讨论区里,学习了很多大佬们求取最优解的算法和各种调优策略,很受启发。我这三次作业,由于很多主观和客观原因,以及自己的精力有限,很多想法都没能去实现,而是采取了比较中庸的做法,稳扎稳打地完成了这三次作业。如果单纯唯结果论的话,求稳或许是一种性价比很高的做法。但是对于学习而言,放弃冒险,就等于放弃了提升自我的机会。希望能在未来的OO学习中,放弃这种胆小保守的做法,勇敢探索。
  • 相关阅读:
    react ts axios 配置跨域
    npm run eject“Remove untracked files, stash or commit any changes, and try again.”错误
    java 进程的参数和list的线程安全
    帆软报表 大屏列表跑马灯效果JS
    帆软报表 快速复用数据集,避免重复劳动
    分析云 OA中部门分级思路和实现方法
    分析云 分段器 只显示一个块的数据
    分析云 更改服务默认的端口号
    分析云U8项目配置方法新版本(2)
    Oracle 创建时间维度表并更新是否工作日字段
  • 原文地址:https://www.cnblogs.com/liujiahe0v0/p/liujiahe_oo_summary.html
Copyright © 2020-2023  润新知