OO第二单元总结

面向对象第二单元总结

第五次作业

本次作业仅涉及到一部电梯的运行,采用的设计是生产者-消费者模型。

  • UML类图

    Class Diagram
    • 生产者:InputHandler线程,即读入数据的线程。

    • 托盘:RequestQueue,用HashMap来实现,key值是请求来自的楼层数,value是请求本身。其中的所有方法都加上了synchronized关键字,用来保证共享数据的线程安全性。

    • 消费者:Elevator线程,即电梯线程,从请求队列中取出请求并且响应。

    • 其他:ElevatorConfig类,用于记录电梯的一些属性,便于以后拓展出不同类型的电梯。Scheduler类,用于管理电梯内请求,完成人的进出行为,并且根据电梯内的请求情况和电梯外的请求情况对于电梯的运行方向做出决策。

  • UML协作图

    5-seq

    在MainClass线程中启动InputerHandler线程和Elevator线程,InputHandler将请求不断放到托盘中,消费者从托盘中取出请求并且执行。当InputHandler接收到null值表明输入已经结束,因此下发停止信号到托盘中,Elevetor线程读取到stop信号之后,如果当前电梯内请求为空,并且requestQueue中为空,则自行结束线程。

    对于避免轮询的办法,采用的是wait-notifyAll的机制,当电梯内部没有请求并且当前等待队列中请求为空并且当前输入还没有停止,则进入wait状态。当InputHandler向缓冲区添加请求或者是下发stop信号的时候均要唤醒正在等待的请求,使得电梯线程能继续读取请求或者自行结束。

  • 调度器分析

    因为本次作业涉及的电梯仅有一部,因此并未设计调度器来分配请求。

  • 关于同步块和锁

    对于共享数据RequestQueue采用的是对于每个方法都加上了synchronized关键字来保证线程安全。

第六次作业

这次的作业涉及到了多部电梯,同时还要求能够动态增加电梯。因为涉及到了多部电梯,因此本次作业添加了调度器类,用于下发请求,同时每一个电梯属于自己的等待队列,这就构成了一个两个生产者-消费者模型的框架。

  • UML类图

    2-UML
  • UML协作图

    第一个生产者-消费者模型

    • 生产者:InputHandler 线程
    • 托盘:WaitQueue ,作为缓冲区存放所有输入读到的数据,作为共享数据,其中的所有方法都用synchronized关键字修饰来保证线程安全性。
    • 消费者:Dispatcher线程,从WaitQueue中获取请求,并根据分配算法分配到合适的电梯中;如果请求是添加电梯类型的请求,则相应的开启新的电梯线程。

    第二个生产者-消费者模型

    • 生产者:Dispatcher线程,寻找最合适的电梯线程并下发请求。
    • 托盘:RequestQueue,本质是每个电梯自己的专属等待队列,也是Dispatcher线程将请求下发的缓冲区,沿用上一次的架构。作为共享数据,其中的所有方法都用synchronized关键字修饰来保证线程安全性。
    • 消费者:Elevator线程,读取自己专属等待队列的请求并且执行请求。

    其他类

    • Scheduler:用于管理电梯内的请求。
    • ElevatorConfig:记录电梯的最大承载数,运行的速度等电梯属性,便于之后作业的拓展。
  • 调度器分析

    • 因为本次作业涉及到了多部电梯,对于请求的处理我采用的是集中分配式,即使用一个分配器线程来完成对于请求的下发。Dispatcher线程在我的架构中担任了两个角色,既是消费者,同时又是一个生产者的角色。“消费”总请求队列中的请求,再将这些请求下发给电梯的等待队列,此时作为一个生产者的角色,通过上述的方式来完成与其他线程的交互和协同配合。

    • 在分配请求的时候,我采用的是捎带+平均的原则。对于Morning和Night模式,均采用平均分配的策略。而对于Random模式下的电梯,采用的策略是首先判断每部电梯当前的状态能否捎带这个请求,如果可以则加入距离*等待队列中请求数量最小的电梯。因此需要满足Dispatcher能够获得电梯的状态,因此电梯在运行的过程中,将需要的状态更新在RequestQueue中,使得Dispatcher能够取得状态信息。

    • 在结束线程的时候,通过从WaitQueue中获取下发的 stop 信号以及在结束自己的线程之前给每个电梯的等待队列 RequestQueue 下发一个 stop 信号,由此完成所有线程的结束行为。

  • UML协作图

2-UML 运行的大致流程是,在MainClass中开启InputHandler线程和Dispatcher线程,由Dispatcher开启电梯线程。当请求输出的时候,由InputHandler线程将请求放到请求队列中等待分配,Dispatcher从缓冲区中取走请求,对于PersonRequest进行请求下发,对于ElevatorRequest则开启新的电梯请求。Elevator线程则从自己的等待队列中取出请求并执行。

对于wait-notify的使用,对于Dispatcher线程来说,如果当前WaitQueue中为空,并且InputHandler尚未下发 Stop 信号,则wait;如果这两个条件都满足,则对每个电梯的RequestQueue下发电梯的Stop信号,并且自行结束。而对于InputHandler线程,将请求加入队列或者下发 stop 信号之后都要唤醒可能在等待中的Dispatcher信号。而对于Elevator信号和上一次作业的行为一样,这里不再赘述。

  • 关于同步块和锁

    因为这次增加了Dispatcher线程,共享数据增加了一个WaitQueue类,同样的给它内部的方法都加上了sychrnoized关键字来保证线程安全。

第七次作业

这次的作业增加了电梯的种类,不同的电梯支持到达的楼层不同,移动的速度不同并且承载量也不同。

整体架构仍然采用的是生产者-消费者模式。

  • UML类图

因为涉及到开启不同的电梯线程,因此对于电梯的种类的区分是使用ElevatorConfig来进行区分,因此增加了Factory类,用于针对不同的电梯来创建对应的电梯属性。同时因为电梯的运行速度的不同,为了缩短运行的时间,提出了换乘电梯的策略,为了满足换乘电梯的需求,新增加了MyPersonRequest类,根据是否需要换乘,是否为第一次换乘等情况,对于getFromFloor等方法的逻辑进行修改,使得原来代码中对于请求的处理不需要改变。

2-UML
  • 调度器分析

    • 在保留了上次作业的逻辑架构的情况下,针对换乘的需求,增加了新的逻辑。为了满足换乘我们需要能够做到在电梯将人运送到换乘的楼层时将新的请求放到WaitQueue中,使得Dispatcher能够重新获得这个换乘的请求,并分配到相应的电梯中。因此我们的电梯线程也拥有了双重的身份,既是一个消费者,同时也可能是一个生产者。

      因此整个流程变为:InputHandler从输入中获取请求,将请求放在WaitQueue中等待消费者Dispatcher分配,Dispatcher从缓冲区WaitQueue中取出指令,并根据请求的fromFloortoFloor选择合适的电梯或者判断是否换乘,下发请求到电梯的队列中。Elevator线程从各自的等待队列中取出请求并执行,将人运送到指定的目的楼层后,人在出电梯之前需要判断是否为需要换乘的人员,如果是则将该请求放到WaitQueue中等待二次分配。

    • 对于分配请求,我针对不同的到达模式选择了不同的分配策略。

      对于Morning模式,在优先级CBA的前提下,即能用C送的,全都塞到C的队列中;如果不能用C完成的,能用B完成的,全部放到B中,剩下BC都无法完成的,再选择使用A送。决定好使用哪一种电梯之后再平均分配到同类型电梯中。

      对于Night模式,能用BC直接送达的,使用BC电梯来送,剩下出发楼层为4-17之间的偶数层的,A号电梯将其送到下一层的奇数层,再换乘B电梯到达1楼。

      对于Random模式,同样优先用BC送人,其余的请求,如果换乘一次能够到达目的地,即如果目的楼层不是偶数,并且请求来源楼层距离目的地的距离大于等于5层,那么将它送到最近的奇数层之后换乘B电梯。

    • 在结束线程方面,调度器还作为一个controller来一次性给所有电梯下发stop信号。因为本次作业实现了换乘机制,因此对于结束的条件就必须考虑到电梯中可能还有需要换乘的人,即在之前的条件中还必须加上当前无需要换乘的人员了。

      我采用的方法是在调度器中增加了一个count变量来记录还有多少人需要换乘,每次取得请求的时候,在分配给相应的电梯前,如果这个请求不需要换乘的,则count保持不变;如果需要换乘,则加一;如果已经是在执行换乘需求的请求,则减一。当count = 0的时候也就表明当前所有的换乘需求都已经执行完毕。

      其他线程结束的条件沿用了前两次作业的逻辑。

  • UML协作图

image

  • 关于同步块和锁

    本次作业沿用了第二次的整体架构,并未有新增的共享数据,因此没有修改。

  • 可拓展性

    这次作业的功能设计和性能分析都已经写在前面了,第三次作业的可拓展性个人觉得还是比较好的。对于新增电梯种类,只需要使用Factory工厂类来新建类,并且电梯内部均是调用ElevactorConfig类来进行一些列操作的。同时如果增加换乘的需求,只需要在Dispatcher类里加上对于MyPersonRequest内换乘楼层的设计即可,并不需要修改架构即可完成。

分析自己程序的bug

在第二单元的作业的三次作业中,我在强测和互测中均为被测出bug。

但是在第七次作业中,我似乎遇到了程序无法正常结束的死锁问题。交上去的时候有几率会爆rtle,但是用那个数据点在本地反复跑了40多次但是一直没有能复现出来。我一度以为是因为死锁的问题,而且是因为对于共享资源的访问造成的死锁,即我们常说的因为获取锁的顺序不同而造成的,因为我一直在往这个方向想,导致我一直没能想出到底是哪里出现了bug。

在ddl的前一个小时的时候,我恍然大悟了,但是因为没有时间改了,而且我并不是十分确定一定是因为这个bug而导致的线程无法正常结束,因此我没有冒险尝试,但是我觉得有极大的概率是下面这个bug造成的。

image

这两段代码分别对应Elevator线程的结束判断和Dispatcher线程的结束判断,看上去逻辑好像没有问题,但是有一个很大的问题:这两段代码是放在线程内的run方法中的。这就会造成一个问题:当Elevator线程运行到第二行代码的时候,也就是判断Dispatcher线程是否下发了stop信号,此时Dispatcher线程并未下发,接下来Elevator线程就要进入wait状态了。但是!有一个问题,如果此时Elevator线程调度走了,换成了Dispatcher线程,它下发了stop信号并且执行了notifyAll。那么这会导致这个唤醒信号被错过,而Elevator线程将会一直处于等待状态而无法正常结束!

我觉得我偶尔会爆的rtle就是因为上面这个bug,但是我很幸运,我这个写法苟过了三次强测和互测,所以我最后并未修正这个隐藏的bug。这也表明我在写多线程的作业的时候对于多线程的运行的不确定性理解还是不够到位。

互测环节

因为这个单元的作业时多线程相关,而且每个人的调度的策略都不同,输出也都是不一定的,对于正确性的判断光靠我自己是不太现实的。我一开始设想的办法是:如果能将每个人的输出进行图形化的展示,将文字的输出用一个图形化界面来展示整个调度的过程,那么就可以直观地判断正确性了。

但是我太菜了,没有实现这个想法quq

这一单元的互测再用上一单元的办法,随机生成样例来检测正确性,对我来说效率就比较低了。因此我觉得相比起前一个单元,多线程观察静态代码或许是一个更为有效的办法,通过对于静态代码的分析会更容易找到漏洞,当然能够复现这个bug又是一大难题了。

心得体会

首先恭喜自己:多线程终于结束了!!!

  • 层次化设计

    首先感觉到自己对于架构的设计已经有点概念了,这三次作业都没有重构,并且三次都做到了迭代开发。个人认为自己对于低耦合高内聚的设计概念也有所运用了,至少对于每个类之间的关系更加明确了,各自的职责更加明确了。

    三次作业都使用的是生产者-消费者模式,并且从单生产者-单消费者模式,逐渐拓展到多生产者-多消费者模式。缺点是对于一些老师和同学介绍的其他模式比如读写锁等模式并未尝试过,而是一直采用synchronized关键字来保证线程安全。

  • 线程安全

    我认为这里的难点就是如何避免死锁,这是困扰很多人的地方,但是我这种wait-notify造成的死锁课程组很少涉及,我认为课程组也许可以提供一点相关的实验或者资料让同学们认识到这个也有几率导致死锁,因为我感觉我周围挺多人都是这个bug造成的rtle问题。我也是直到最后一小时才感觉可能是这个问题,最后采用了鸵鸟算法:无所作为,无视死锁。(其实是来不及改了)

posted @ 2021-04-24 14:24  膝盖受损  阅读(67)  评论(1编辑  收藏  举报