OO第二单元总结

BUAA_OO第二单元总结

​ 本单元的任务是迭代开发多线程电梯,在这个过程中我学到了设计多线程程序,特别是处理线程安全问题,以及处理多线程设计的问题(本单元中我的电梯能跑,但慢的离谱,就在这里出现了问题,后面详细分析),并且还了第一单元的债,切身体会到架构的重要性,耦合给程序调整带来的麻烦。

​ 本单元具体任务及总结的难点如下:

  • 第一次实现模拟单部多线程电梯运行,需要根据到达模式不同调整策略,我认为难点在对电梯性能的约束上。我没有完成第一次作业,原因后续详细分析

  • 第二次实现模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。我认为的难点在并发运行的框架下如何实现好的调度策略上。

  • 第三次实现模拟多部不同型号电梯的运行,其中不同型号电梯可停靠楼层不同。我认为的难点在增加条件后,如何调整以实现好的调度策略上。

一、设计策略

1.整体架构

​ 在本单元三次作业中,基本实现了迭代开发,架构基本一致。

类名 用途
InputThread 用官方包读取输入,并将读取到的请求放入Scheduler中的waitQueue
Scheduler 静态调度器,将waitQueue中请求分配给各电梯对应的processQueue
Elavator 电梯线程,根据策略类运行,处理对应processQueue中请求,完成乘客接送
Stratge 电梯运行策略类,根据电梯状态及processQueue中请求,确定电梯运行方向

2.同步块的设置和锁的选择

  • 为什么需要同步块? :在多线程程序中,线程间共享的数据数据不安全,需要同步块保护。

  • 锁的选择:我沿用了第一次作业的用synchronize关键字加锁,经过课程的讲解我学到了更多的加锁方式,如逻辑锁lock等。

  • 在我的程序中:线程间共享的是Scheduler类,我就把同步块加在了Scheduler上,以维护在waitQueue中增加请求,取出请求的数据安全。

  • 事后反思:我把锁加“大”了,Scheduler类中真正进行数据修改的对象是waitQueue对象。锁加大之后,不必要的代码如:Scheduler决策把请求分配给哪个电梯的代码段也会进入临界区,导致性能上损失

  • 同步块的处理语句:怎样写wait-notify可以保证程序正确运行,不出现死锁,线程在非预期时机被唤醒?

    ​ 我踩过的坑:

    • 少加wait,导致轮询
    • 多加notifyall,导致程序逻辑混乱。初写多线程没有经验,朴素的认为为了避免死锁,多加notifyall。但实际上一是不符合实际逻辑,电梯取出对应的请求没必要通知其他电梯,二是无效,我弄混了blocked与WAITING状态,notifyall只能唤醒WAITING状态,解决不了由于同步块写乱导致有线程始终拿不到锁的问题,三是调试的时候自己给自己找麻烦

3.调度器设计

我认为我的调度器设计可以说是失败的,在第三次强测数据点输入70s结束的情况下仍能运行超时,硬生生把并发写出了同步...总结一下我的反思吧。

  • 基本思想:调度器的任务 避免只有一个或少个电梯运行全部请求,尽量使请求在可能范围内平均分配到电梯,电梯都在运行。

  • 调度器如何实现与其他线程交互?:我实现的是静态调度器,电梯运行前调用方法使调度器分配请求。调度器通过获取电梯线程状态,把请求加入电梯的processQueue。

  • 第一次作业:单电梯实际用不上调度器,此处只是个空壳。但我还是踩坑了,把调度器写成了上帝类,高度耦合,电梯移动甚至都要依赖调度器,架构混乱也导致了我未能完成第一次作业..

  • 第二次作业:作业要求处理多部电梯的调度,且支持动态增加电梯。在混沌模式:电梯自由竞争程序实现调度 之间,我选择了后者,并且实现的不太好,导致强测有点RTLE。

    ​ 我写的调度器:负载优先调度,从waitQueue中取出请求放入当前processQueue + 电梯里人数 最少的电梯

    ​ 优化:计算各电梯完成请求的时间,加入所需时间最少的电梯;改为自由竞争模式

  • 第三次作业:作业要求在增加电梯可到达楼层类型条件下,进行多部电梯的调度。

    我写的调度器:请求到达队列后,根据请求的可到达楼层决策分配电梯的类型,优先级C>B>A。

    实现换乘:当前电梯的可到达楼层中不包含请求的该方向的下一楼后,从电梯队列中删除该请求,new一个请求,改fromfloor重新加入waitQueue。

    我踩的坑:少考虑调度因素 + 缺乏测试手段 ,把并发写出了同步,导致加了换乘也未能提升性能。

    只朴素的考虑到C类电梯最快,可能条件下尽可能给C类电梯,导致分配严重不均,C类电梯一直跑,其他类电梯没机会跑。在morning模式下,请求都从1层出发。即其他电梯即使当前空闲,也要等待C类电梯将请求送到C类电梯最大可到达楼层后,才有运行的机会。

4.第三次作业架构设计分析

UML类图

UML时序图

扩展性分析

  • 电梯型号的扩展性: 通过继承父类Elvator,只要更改其中的参数,如到达楼层,运行速度,即可实现更多电梯类型的扩展

  • 电梯运行策略的扩展性:电梯的运行统一调用Stratge类。我把Stratge类里根据电梯到达模式Morning Night Random不同,写了不同的策略。若有新到达模式,只需增加即可。

  • 反思:在这次作业里,我在调度策略的算法层面 与 代码实现层面 的扩展性上都做的不好

    • 调度算法的扩展性:

      ​ 正如上文所提 我写的调度算法类似

      	if(符合c类&电梯未满)  放c类处理队列
      	else if(符合b类&电梯未满) 放b类处理队列
      	else 放a类处理队列
      

      一是判断调度的因素太少,没有做到综合考虑各电梯的状态如运行方向、负载等因素

      二是不具备扩展的可能,条件一变全得重写

    • 代码实现层面得扩展性:
      一个致命的点是用if-else嵌套进行调度判断,就制约了思考,一想写个综合的算法得写一长串,调试也麻烦,不具备可读性,索性就朴素得写了。二是逻辑很难写的周全,满足if则后面的if条件都不判断了,但实际情境是在简单的电梯情境下,也有比较复杂的问题如满足c类,但a类电梯还空闲不应分配给c类等。硬要实现又要在if里加条件,又给自己找逻辑上可能出错的麻烦...

    • 学到的好算法:

      赋分算法:给各因素赋权值,最后按总体得分分配请求。

      这种算法能综合考虑多种因素,并且扩展性好,条件改变直接改权值或者增减项即可

5.分析自己程序的bug

​ 这三次作业我出的bug全在调度器上,第一次因为调度器架构失败,第二、三次单纯因为调度器策略设计的不好,导致电梯跑超时。具体详见3.调度器设计,做了问题分析,4.中做了反思,解决。

6.分析自己发现别人程序bug所采用的策略

​ 这单元作业中我没能hack成功别人的代码,策略不具价值,写下我对多线程hack的理解吧。

​ 线程安全问题:由于多线程错误有些是概率出现的,直接暴力跑不一定方便。我想白箱静态看别人代码这种方法比较简单。只需重点关注wait之后有没有被唤醒机会,锁加的合不合理,线程结束条件是否合理(是否会因为输入线程提前结束...)等即可。

7. 心得体会

这个单元从开始的知道多线程是并发的,到后来写程序自己”掌控“设计并发程序,完成任务,认知是不断加深的。

  • 如何保证多线程程序的正确性?:关注线程安全问题,加锁线程间共享数据的保护,用java封装好的BlockingQueue,ConcurrentHashMap等线程安全的容器,确保不死锁,不意外结束。

  • 如何设计好一个多线程程序?:

    • 最起码,最重要的一点:架构>>其他
    • 工程设计原则:架构完整前不要考虑具体的优化。我第一次作业的时候就一心想实现线程安全类,结果没完整弄明白呢就让调度器成了上帝类加了很多锁,写了wait还是CTLE,最后重构才解决问题。
    • 多线程模型: 生产者消费者模型:让我理解”缓冲区“对性能提升的作用,读者写者模型
    • 不要被底层代码实现制约:复杂问题别用if-else嵌套
    • 设计思维:不断学习 不断成长,自己算法实现的不好找同学交流
    • 测试手段:评测机真的有必要,idea纯手动输入测调度策略的优劣太太太麻烦了,我脑子里没考虑过极端数据,也没测过多点儿的数据,自然调度策略实现失败。

posted on 2021-04-25 15:32  骤雪封光  阅读(97)  评论(1编辑  收藏  举报

导航