2021_BUAAOO_第二单元总结
一、同步块和调度器分析
第一次作业

# 第二单元总结
以上是第一次作业简化后的UML类图和顺序图。其中,TimeTable类是共享类,线程类TimetableInput会将输入的请求存入TimeTable,线程类Elevator会取走TimeTable中的请求交给Tatic策略接口,策略接口会调用函数得到下一个要执行的请求。
同步块分析
基于以上分析,我的同步块主要存在于TimetableInput类和实现了Tatic接口的三个策略类(本质上是存在于Elevator类)中。在TimetableInput类中,我给Timetable类的实例对象timeTable加了物理锁,并在同步块内部进行了向timeTable中加入请求以及设置结束标志的操作。
在MorningTatic、NightTatic、RandomTatic中,我同样给TimeTable的实例对象加了物理锁,在同步块内部读取和取得TimeTable中存储的请求信息,经过计算后返回给Elevator类下一个应该执行的请求。
调度器分析
这次作业我没有专门设置调度器,所有的调度策略都写在了策略类中,而每个电梯都自带一个策略属性来进行调度操作。其中,策略类内部依照look算法具体实现下一个执行请求的选择。也就是说,我将调度器内嵌在了电梯内部,由电梯自身根据当前的候乘信息和电梯信息来分析接下来将要采取的行动。
第二次作业


以上是第二次作业简化后的UML类图和协作图。其中,WaitingQueue和ProcessingQueue是共享类,其作用分别为存储还未分配的请求以及存储已经分配到某个电梯但是还没有被处理的请求。线程类Input会将输入的请求加入WaitingQueue中,线程类Collector会从WaitingQueue中取得待分配请求,然后分配到每个ProcessingQueue中,线程类Elevator会从ProcessingQueue类中取得请求,然后交由Tatic分析。
同步块分析
这次作业的同步块存在于Input类、Collector类、实现了Tatic接口的三个策略类(本质上是存在于Elevator类)中。
在Input类中,我给WaitingQueue的实例对象加了物理锁,在同步块中进行了向WaitngQueue中加入请求和设置结束标志的操作。
在Collector类中,我给WaitingQueue以及每个ProcessingQueue的实例对象都加了物理锁。在WaitngQueue的同步块内部,Collector进行了从WaitngQueue中取出请求的操作;在ProcessingQueue的同步块内部,我进行了向ProcessingQueue中分配请求和设置结束符的操作。
在MorningTatic、NightTatic、RandomTatic中,我给ProcessingQueue的实例对象加了物理锁,在同步块内部读取和取得ProcessingQueue中存储的请求信息,经过计算后返回给Elevator类下一个应该执行的请求。
调度器分析
相对于第一次作业,我把TimeTable类重新命名为ProcessingQueue类,每个电梯对应有自己的一个ProcessingQueue待处理队列,能够根据处理队列的请求信息像第一次作业中那样做出行动。但是这一次Input类并不直接向每个处理队列中加入请求,而是需要先将请求加入到WaitingQueue等待队列中,然后由调度器类Collector依照平均分配的原则将请求分配给每个处理队列。这样就对第一次作业进行了很好的复用,每个电梯不需要去关注别的电梯的状态,只需要知道自己需要完成哪些任务。而电梯间的调度全都交给了Collector类。
第三次作业


以上是第三次作业简化后的UML类图。第三次作业的总体架构与第二次作业没有任何区别,只是添加了几个辅助类来实现Collector中的调度。
同步块分析
Input类和三个策略类中的同步块与第二次完全相同,只在Collector类中做出了改变。
在Collector中,我给WaitingQueue的实例对象加了物理锁,在同步块内部进行了取得请求并存入Collector中的操作。我给每个ProcessingQueue的实例对象加了物理锁,在同步块内部进行了取得ProcessingQueue信息和分配Collector中存储的请求的操作。
调度器分析
调度器的总体架构与第二次作业几乎没有区别,但是这一次Collector需要从ProcessingQueue中获得信息以进行分配请求的操作。我这次分配的策略是:从WaitingQueue中取得请求之后,依据Folyd算法规划出用时最短的运送路线(该路线不考虑中途的等待时间),并依据该路线进行拆分并存储在Collector类内部。根据拆分的结果,Collector会将拆分后的请求分配给用时最短的电梯。如果某类电梯有多部,则在所有该类电梯内部平均分配。对于那些需要换乘的请求,ProcessingQueue中会存储前序请求的到达信息,Collector会从ProcessingQueue中取得这些信息后激活那些换乘请求的后续部分进行分配。调度器将请求分配给ProcessingQueue之后,电梯又可以像前两次作业一样进行运行了。
二、第三次作业架构分析
UML类图

UML协作图

功能设计方面,我将请求的调度分为了两层,第一层由Collector主调度器进行宏观上的调度,通过对待分配请求的分析,通过floyd算法静态决定其换乘策略,然后对请求进行拆分,最后再分配进入每一类电梯。分配进入每一类电梯之后再进行平均分配到每一台电梯的待处理队列。第二层调度上,每一台电梯根据内置的调度器依照look策略决定将要执行的请求。
性能设计方面,由于Collector主调度器将请求分配到每一台电梯的待处理队列后就不会占用待处理队列,从而让电梯能够调用待处理队列选择下一步行动。因而第一层调度和第二层调度的耦合度比较低,相互间的“妨碍”程度不高,从而能够获得比较良好的性能。
在可扩展性方面,由于采用了两级调度,因而具有比较好的可扩展性。一旦加入新的要求,只需要对主调度器Collector进行改进即可实现新的分配。只要将请求分配到了待处理队列上,电梯仍旧可以采取之前的二级调度策略进行运转。
三、bug分析
线程bug
在第一次作业中,我在TimeTableInput类中向TimeTable(候乘表类)中加入请求后会进行wait等待,以释放CPU和TimeTable。但是我等待之前没有进行notifyAll来唤醒阻塞的线程,从而造成了死锁。解决这个bug的方式是在wait之前notifyAll。
逻辑bug
在第二次作业我采用了look算法来进行电梯的二级调度。我的look算法实现基于ProcessingQueue中的请求按fromfloor从小到大的顺序排列。我通过每次加入新请求的时候利用插入排序的思想将其放入合适位置。但是如果新请求的fromfloor比其它所有请求都大的话会插入倒数第二位,从而造成了bug。这个bug是排序错误造成的。解决的方法是对新请求比所有请求都大的情况进行特判,如果符合就将其插入到最后一个。
我第二次作业还有一个bug是在电梯上没有乘客时,当第一个人上来之后不会改变运行方向。由于我采用了look算法,因而如果在某一层所有乘客下电梯且有乘客上电梯,那么上电梯的这个乘客就会与look算法选择的最远乘客产生冲突,从而电梯会交替将这两个乘客的请求当做主请求,因而产生死循环。修复bug的方法是当电梯为空且有乘客上电梯时,立刻根据该乘客请求的方向调整电梯的运行方向。
四、hack策略
测试策略
我hack的策略主要集中在投放两种类型的数据:短时间内有大量请求到达、各种请求到达的时间间隔很大。其中每种类型的数据都对三种模式(Morning、Night、Random)分别构造了对应样例。这种方法在测试自己代码bug的时候具有较好的效果,但互测的时候由于大家的代码基本都已经通过了相关测试,因而效果一般。
发现线程安全相关问题的策略
主要是通过读他人的代码具体分析。先通过IDEA自动画他人代码简单的UML类图来分析类之间的关系,找出共享类,然后找出共享类被使用的同步块,定位各个synchronized 和wait所在的位置,分析可能执行的顺序,从而定位bug。
测试差异之处
这次的测试与第一次最大的区别就是测试结果难以稳定复现,因而难以采用生成随机样例的方式进行测试,必须针对相应代码针对性构造测试。这就要求测试的时候必须仔细的阅读和分析相应的代码,而不能通过黑盒测试来检测正确性。
五、心得体会
线程安全方面
线程之间的交互必须通过共享对象来进行,而不能线程之间直接相互调用对方的方法和属性,否则可能会产生一些难以理解的问题。而线程调用共享对象的时候要做好保护,保证一个线程访问共享对象的时候另一个线程不能访问该对象。同时要注意好共享对象的调用顺序,注意好wait和notify的使用,避免出现死锁。
层次化设计
这次作业我采用了二级调度的策略,一级调度器执行分配请求的任务,二级调度器具体决定电梯要执行的请求。这种请求在这次作业中取得了比较好的效果——自第二次作业这种二级调度器的架构确立下来之后,第三次作业仅仅在一级调度器上进行了修改就达到了效果,显示出该架构较好的稳定性。
我再一次体会到了分层设计的重要性——如果能够在一开始确立一个比较好的架构,那么后续的工作就会达到事半功倍的效果。

浙公网安备 33010602011771号