顾晨宇

 

OO第二次博客作业

OO第二次博客作业

OO课程的第二单元是基于Java多线程的电梯调度专题,从第一次作业的单部电梯调度,到第二次作业的多部电梯调度,再到第三次作业的多部不同型号的电梯的换乘调度,一步步使得自己搭建的电梯系统能够应对更多样化的需求。接下来,我将从六个方面对这三次作业进行总结:

总结一:同步块与锁

三次作业均采用生产者-消费者模式,且基本没有经历重构,所以整体架构类似。输入模块作为生产者,电梯和调度器为一个整体作为消费者,等待队列类作为线程安全类。因此,作业中的同步块和锁均出现在等待队列类中,下面对该类进行分析。

由于输入模块会将请求添加到队列中,而电梯会取出队列中的请求,因此,对于请求队列如果不加锁,会导致线程不安全。而对于以下几类方法,需要保证队列的线程安全:

  • 向队列中增加请求

  • 从队列中取出请求

  • 统计队列中的请求数/是否有请求

经过考虑,我采用synchronize关键字对passengerQueue(请求等待队列)进行加锁,在上述三类方法中设置同步块,保证对passengerQueue进行操作时不会出现线程不安全的情况。

在同步块的处理语句中,增加请求的方法会先添加进队列,再notifyall所有加了锁的同步块;而取出请求的方法会先检查是否有请求,如果是则直接取出,否则,wait等待新请求加入时的唤醒信号。

但是,加锁之后随之而来的是隐藏在代码中的死锁现象,即一直wait等待而无法被notify唤醒,而这就极大可能出现RTLE的错误。

对于死锁的避免,我采用“双重唤醒”机制,即在新请求添加时进行一次notifyall,在标准输入结束后进行一次notifyall,保证了一般情况下的死锁现象(对于临界状态的死锁,我出现了bug,将在后面的bug分析中再进行讨论)。

总结二:调度器设计

调度器主要作为电梯与等待队列的“中间商”,减轻电梯的逻辑压力。

与调度器有直接联系的是等待队列类和电梯类,等待队列类是一个线程安全类,而电梯类为一个Thread类,调度器通过从等待队列中取请求、统计询问等方式与等待队列类进行交互,通过提供运行策略和将请求加入电梯等方式与电梯类进行交互。

同时,调度器也为了提升可移植性,便于组装不同类型的电梯,通过不同的调度策略应对不同的需求。下面将对三次作业设计的调度器及其调度策略进行相关总结分析:

第一次作业:

这次作业只需要处理单部电梯的调度,因此调度器的设计相对简单,对MorningNightRandom三种模式设计了相应的调度算法:

  • Morning:由于每两人到达时间间距小于等于2s,因此采用等待策略,每取出一个请求,等待2s,再取出新的请求,当电梯载满或者输入结束,则启动电梯,本趟全部送达后,返回一楼接收新的乘客直到乘客全部运送完毕;

  • Night:找到当前所在楼层最高的请求,将该乘客装入电梯,在向下移动的途中,若当前楼层有请求并且电梯未载满则取出请求,当电梯下降到一层将本趟乘客全部送达后再重复前面操作直到所有乘客均被送达;

  • Random:对Look策略进行一定修改:即若电梯为空,则取出一个请求作为主请求,在运送途中,若碰到可捎带的请求且电梯未载满,则取出该请求,电梯边运行边判断(是否有乘客要上下电梯、运行方向是否还有请求),直到运行方向上无请求,此时改变运行方向,重复以上操作直到全部乘客运送完毕。

调度器性能分析:

由于Morning模式非常依赖输入请求,导致了很难有应对所有情况的最优调度策略,因此本次作业Morning模式的五个强测点性能均不高,而其他两种模式的性能均还不错。

第二次作业:

这次作业需要处理多部电梯的调度,且可以动态增加电梯。我主要采用集中式调度策略,即只维护一个主等待队列,通过调度器让电梯之间进行竞争(内卷)来获取请求。本次作业也对三种模式做了分情况的处理:

  • Morning:由于是多部电梯同时运作,因此放弃了上一次作业抠门的“等待策略”。每部电梯在一楼时,在不满载情况下把能取出的请求都取出,直接处理(而不是每隔2s等待新请求),直到所有乘客均被送达;

  • Night:在此模式下使用了分布式调度,每次在一楼时,从主等待队列中取出6个放入自己的privateQueue中,按照privateQueue中的请求进行调度,直到所有乘客均被送达;

  • Random:此模式还是采用集中式调度,沿用第一次作业的策略,几乎没有进行修改。

调度器性能分析:

在本次作业的公测中,三种模式的调度器均表现不错,除了一个测试点,都拿到了较高的性能分。经发现,改测试点的请求大多为相近楼层之间的运送,Random模式在这种情况下可能出现效率不高的情况。

第三次作业:

这次作业需要处理三种型号的多部电梯的调度,且依旧可以动态增加某一型号的电梯。本次作业为了减少工作量(其实是太忙了时间不太够),将三种模式合并成一种调度策略,对三种型号的电梯制定不同的运行方案(运行方案的不同体现在电梯类内部,而在调度器中还是统一处理):

  • A类型电梯:按照前两次作业的方法运行;

  • B类型电梯:如果某请求的目标楼层不在当前楼层的上下层(避免使乘客重复进出电梯)则让该乘客上电梯;如果某请求目标楼层为当前层或当前层的下一层(取决于运行方向)则让该乘客下电梯;

  • C类型电梯:相当于只能到达6层的电梯,在可以到达的楼层间按照A类型电梯的运行方式运行。

调度器性能分析:

本次作业需要我们制定换乘策略,但是根据同学们的反映,没有换乘也能拿到较高的性能分,因此本次公测数据对调度策略区分度并不高。本次的公测除了RTLE了一个点,其他均通过且拿到了较高的性能分。

总结三:第三次作业架构设计的可拓展性

第二次作业在基本的生产者-消费者模式的基础上增加了一个中央处理类CenterUnit(就像是计组的CPU),用来分配和调度各种资源(电梯、集中队列、分布队列、调度器),而第三次作业沿用了这样的设计。需要说明的是,CenterUnit并没有复杂的逻辑,它就像一个车间,只是单纯地将各种资源组装联系到一起,方便不同资源间的沟通访问。

下面先给出第三次作业的UML类图和UML协作图,再结合这两张图对第三次作业的架构设计进行分析:

UML类图:

UML协作图:

架构分析:

优点:
  1. 中央处理类CenterUnit能够方便地创建和管理各种资源(包括可能产生的新资源);

  2. 调度器从电梯中分离,通过改写调度器可方便创建不同类型的电梯;

缺点:
  1. 线程安全类RequestQueue不够“安全”,对于一些极端情况,可能会产生bug;

  2. 类的抽象度较低,如ElevatorRequestQueue,不利于根据不同需要进行拓展;

可拓展性总结:

综合分析优点和缺点,第三次作业的架构在宏观层面上有利于资源的叠加,但是每个资源类内部耦合度较高,不利于内部的拓展。

总结四:分析自己程序的bug

第一次作业:

第一次作业是第一次接触多线程,所以我写的时候”步步留心,时时在意“,在自己debug时遇到过死锁现象,索性及时解决,在强测和互测中均没有被找到bug。

第二次作业:

第二次作业尽管在强测中没有被找到bug,但是在互测中被同一个人hack了5次(最后发现是同质bug,太不友好了),该bug产生原因和解决方法如下:

bug产生原因:

如果模式和请求信息同时到达,例如:

[1.0]Random
[1.0]242-FROM-10-TO-19
[1.0]ADD-22-A
[1.0]120-FROM-4-TO-18
[1.0]148-FROM-5-TO-17
[1.0]86-FROM-9-TO-17

会导致输入终止判断变量isInputEndFlag放入CenterUnit的“资源池”中太迅速了,去notifyall等待的线程的时候,线程甚至还没开始,等到线程准备工作时,没有人来notify它们,最后等得花儿都谢了,导致了RTLE。——在临界处产生了死锁。

bug解决方法:

知道了bug的产生原因,解决起来就容易了,调整了一下线程执行的顺序和线程终止的条件判断,使得isInputEndFlag产生时间晚于进程开始运行的时间,最终解决这个临界时刻的bug。

第三次作业:

第三次作业又大意了,在强测中挂了一个点,但实际上是两个点,由于多线程运行的不稳定性,其中有一个点刚好在测试时碰巧过了,不过这两个点也算是同质的bug。

bug产生原因:

在本地好几次运行之后,终于复现出来了(本地复现真的好难啊)。本次作业采用了一定的换乘,电梯停止运送后要等待其他电梯把所有乘客都送达之后再一起结束进程(防止想换乘但没适合的电梯的情况),在所有电梯都结束之后,有一种临界情况下,所有电梯都已经停止了,但电梯线程仍然不结束。

bug解决方法:

原本的代码仅在某一电梯停止时判断是否结束所有电梯线程,针对这个bug,增加了在所有电梯都停止时,额外再判断一次结束所有电梯线程,相当于多加了一道防线,解决了这个bug。

总结五:分析自己发现别人程序bug所采用的策略

策略一:

使用前几次公测的数据,根据本次作业进行适当修改,作为测试用例;

例如:(第二次作业强测中伤亡惨重的data9)

[0.0]Random
[0.0]ADD-4
[0.0]ADD-5
[180.0]1-FROM-1-TO-20
[180.0]2-FROM-1-TO-20
[180.0]3-FROM-1-TO-20
[180.0]4-FROM-1-TO-20
[180.0]5-FROM-1-TO-20
[180.0]6-FROM-1-TO-20
[180.0]7-FROM-1-TO-20
[180.0]8-FROM-1-TO-20
[180.0]9-FROM-1-TO-20
[180.0]10-FROM-1-TO-20
[180.0]11-FROM-1-TO-20
[180.0]12-FROM-1-TO-20
[180.0]13-FROM-1-TO-20
[180.0]14-FROM-1-TO-20
[180.0]15-FROM-1-TO-20
[180.0]16-FROM-1-TO-20
[180.0]17-FROM-1-TO-20
[180.0]18-FROM-1-TO-20
[180.0]19-FROM-1-TO-20
[180.0]20-FROM-1-TO-20
[180.0]21-FROM-1-TO-20
[180.0]22-FROM-1-TO-20
[180.0]23-FROM-1-TO-20
[180.0]24-FROM-1-TO-20
[180.0]25-FROM-1-TO-20
[180.0]26-FROM-1-TO-20
[180.0]27-FROM-1-TO-20
[180.0]28-FROM-1-TO-20
[180.0]29-FROM-1-TO-20
[180.0]30-FROM-1-TO-20
[180.0]31-FROM-20-TO-1
[180.0]32-FROM-20-TO-1
[180.0]33-FROM-20-TO-1
[180.0]34-FROM-20-TO-1
[180.0]35-FROM-20-TO-1
[180.0]36-FROM-20-TO-1
[180.0]37-FROM-20-TO-1
[180.0]38-FROM-20-TO-1
[180.0]39-FROM-20-TO-1
[180.0]40-FROM-20-TO-1
[180.0]41-FROM-20-TO-1
[180.0]42-FROM-20-TO-1
[180.0]43-FROM-20-TO-1
[180.0]44-FROM-20-TO-1
[180.0]45-FROM-20-TO-1
[180.0]46-FROM-20-TO-1
[180.0]47-FROM-20-TO-1

策略二:

使用“时间强度高”的数据,就像我第二次作业出现的bug一样,一些处于时间临界值区域输入的或是特殊时间输入的数据可能会找出对方的bug;

例如:(第二次作业被hack的数据点)

[1.0]Morning
[1.0]8-FROM-1-TO-20
[1.0]23-FROM-1-TO-20
[1.0]44-FROM-1-TO-20
[1.0]31-FROM-1-TO-20
[1.0]15-FROM-1-TO-20
[1.0]63-FROM-1-TO-20
[1.0]66-FROM-1-TO-20
[1.0]12-FROM-1-TO-20
[1.0]49-FROM-1-TO-20
[1.0]ADD-5
[1.0]ADD-9999
[1.0]45-FROM-1-TO-20
[1.0]47-FROM-1-TO-20

策略三:

使用“流量强度高”的数据,短时间内输入大量数据(包括乘客请求和电梯请求),在测试对方电梯调度性能的同时,也能找出对方电梯运行时的一些bug;

例如:(篇幅所限,有所省略)

[1.0]Random
[2.0]314-FROM-13-TO-7
[2.1]346-FROM-19-TO-4
[2.2]ADD-377
[2.3]83-FROM-19-TO-1
[2.4]239-FROM-13-TO-11
[2.5]192-FROM-11-TO-19
[2.6]349-FROM-14-TO-1
[2.7]335-FROM-14-TO-15
[2.8]368-FROM-2-TO-6
[2.9]13-FROM-1-TO-7
[3.0]266-FROM-7-TO-19
[3.1]157-FROM-6-TO-15
[3.2]59-FROM-4-TO-11
[3.3]342-FROM-1-TO-8
[3.4]117-FROM-16-TO-14
[3.5]66-FROM-6-TO-16
[3.6]202-FROM-10-TO-1
[3.7]383-FROM-12-TO-5
[3.8]72-FROM-6-TO-8
[3.9]ADD-538
[4.0]140-FROM-5-TO-15
......

总结:

第一单元的测试是静态的,而本单元的数据是动态输入的,且由于多线程的不确定性,即使是相同的数据,每次的测试可能会出现不同的结果。另外,与第一单元不同的是,本单元的大部分bug其实并不是普通的WA,而是由于多线程操作不当引起的RTLE、CPUTLE等bug,因此hack的重点也放在了后。

总结六:心得体会

在吸取了第一单元重构了三次的惨痛教训后,本单元的作业值得庆幸的一点就是:没有进行重构!

在本单元开始前,助教就提到过:如果第一次电梯作业架构得好的话,后面两次会变得很简单。而我也是听了助教的这句话,第一次作业花了将近三天时间才完成,但是,第二次作业只花了一天、第三次作业甚至只花了四小时(主要是每多花时间考虑优化,不过最后事实证明,优化的效果也很微弱)。

但是,有许多方面还需要改进:第一单元强调多次的层次化设计我仍然没有在本单元中好好实践;方法的设计不优,导致其中的判断、循环层数过多;对多线程的设计也并没有达到很好的效果(有时甚至觉得有点歪打误撞)……

OO课程已经过半,希望能在后半段学习过程中不仅能学习新知识,还能改正过去的缺点吧!

posted on 2021-04-24 14:42  顾晨宇  阅读(69)  评论(1编辑  收藏  举报

导航