OO 第二单元总结

OO 第二单元总结

第一次作业

要求

实现单部可捎带电梯。

设计思路

这次作业中一共包含电梯和输入两个线程。GetRequest类读取输入的数据并将乘客加入到楼层类Floor和全局的等待队列WaitQueue中,电梯根据楼Floors中的乘客和电梯内乘客inElevator的情况判断如何移动。

关于电梯的移动策略,当电梯内有乘客时,移动到目标楼层最近的楼层;当电梯内无乘客而Floors中有乘客时,移动到最近的有乘客的楼层;当Floors中无乘客且输入未结束时等待;均无乘客且输入结束时结束进程。

关于电梯的捎带策略,电梯每经过一层,输出arrive-楼层时,判断当前楼层中是否有目的地方向相同的乘客。如果有,就停止移动,进行开门,进人,关门的操作;否则继续移动到下一层。

关于三种不同模式,我的设计中区别很小。在Morning模式下,只有在一楼等待的乘客到达最大载客量或输入结束时才开始接人,否则等待;在电梯内无人且不在一楼时会自动前往一楼。在Night模式下,只有当输入结束后电梯才会开始运行,且每次会从高到低搜索Floors并前往最高的有人的楼层。

同步块分析和锁的选择

由于刚开始没有太掌握多线程到底要如何实现,所以我刚开始时非常简单粗暴的把两个线程中while(true)循环里的所有内容都用synchronized括了起来(但实际上并不需要锁那么多的地方,是我学艺不精了),导致我的程序中Elevator线程和GetRequest线程是不能同时运行的,当然这个问题在后面几次作业中已经修改了。

关于锁的选择,由于GetRequest线程读取乘客后存入到Floors中,Elevator线程每次直从 Floors中读取乘客,所以我的两个线程中都选择了锁Floors。由于全局的等待队列WaitQueue的相关写操作都必然伴随着对Floors的写操作,所以只需要锁Floors既可。

调度器设计

我的电梯的移动和控制乘客进出都是通过电梯内部进行行为判断。

在Elevator线程的每个while(true)循环中,电梯先判断是否需要等待。如果不需要等待,再判断是否需要开关门(及判断当前楼层有无乘客上下电梯)。然后获取下一个目标楼层并进行移动。这次作业中只有一台电梯且没有什么过多的限制,所以策略很简单。我主要根据的是就近原则,电梯优先满足需要移动的距离最近的乘客的要求。

关于电梯的移动策略,当电梯内有乘客时,将距离楼层最近的楼层作为目标楼层;当电梯内无乘客而Floors中有乘客时,移动到最近的有乘客的楼层;当Floors中无乘客且输入未结束时等待;均无乘客且输入结束时结束进程。

关于电梯的捎带策略,在移动过程中,电梯每经过一层,输出arrive-楼层并判断当前楼层中是否有与电梯目的地方向相同的乘客。如果有,就停止移动,进行开门,进出人,关门的操作;否则继续移动到下一层。

关于三种不同模式,我的设计中区别很小。在Morning模式下,只有在一楼等待的乘客到达最大载客量或输入结束时才开始接人,否则等待(这里是为了避免电梯接了一个人就开始移动而要跑更多的趟数,我个人感觉应该是节省了时间的吧……);在电梯内无人且不在一楼时会自动前往一楼。在Night模式下,只有当输入结束后电梯才会开始运行,且每次会从高到低搜索Floors并前往最高的有人的楼层。因为我的电梯的捎带请求都是在路过那一楼的时候即时判断的,所以只需要确定好最终目的地就好,路上可以捎带的人都会自行上电梯。

架构分析

UML类图

Main:初始化时间戳,创建Elevator线程和GetRequest线程。

GetRequest:读取乘客信息并存入到Floors中。(Floors是楼层类Floor的一个List集合)。

Elevator:从Floors中读取乘客信息,并根据信息运送乘客。floor为当前所在楼层号,floors为楼层集合,waitQueue为全局等待队列。inElevator为电梯内的乘客队列,type为当前所处模式,direction为当前电梯运行方向,number为当前电梯内乘客数。

Floor:楼层类,存储每个楼层中等待的乘客,可以进行是否有乘客需要上楼/下楼,楼层是否为空,加入乘客,移除乘客,取某一乘客信息的操作。

WaitQueue:等待队列,包含一个PersonRequest的List集合和一个成员变量isEnd(isEnd只有在判断全局的等待队列是否结束时会用到)。

Class Metrics

由图可见主要是Elevator线程的复杂度较高。

Method Metrics

因为方法太多所以只保留了复杂度较高的部分。可以看到主要是run()和randomGetRequest()的复杂度较高。run()是因为我第一次作业的时候没有把移动,开关门等过程封装起来,导致方法非常冗长。randomGetRequest()是因为我一开始想为三个模式分别写三个GetRequest()方法,但是写到后面又发现这三个方法没有多大的区别,我就一起写在这一个方法里面了,导致这个方法里充满了各种各样的if…else判断,复杂度就上去了。

UML顺序图

第二次作业

要求

多部同种类电梯的调度。

设计思路

这次作业相对于第一次作业的区别主要是从一台电梯变为了多台电梯。我的设计中电梯的移动和获取乘客的策略仍然是放在我的电梯内部(这将贯穿我这一单元的始终)。由于我的电梯是采取的自由竞争的方式,所以为了防止多部电梯想要带走同一名乘客,导致有的电梯到达目标层却发现没有乘客需要上电梯而白白浪费时间的情况,我想要为每一名乘客多设置一个elevator的属性。因为课程提供的官方包中的PersonRequest是只读的无法修改,所以我重新定义了一个Person用于存储乘客信息。

乘客的elevator属性初始设置为null。为电梯新增一个属性vip,初始设置为0,记录已经被标记但是还没有进入电梯的乘客数量(防止电梯到达目标楼层后发现由于捎带导致电梯已满人的情况出现)。当电梯内没有乘客时,电梯会遍历Floors寻找最近的存在elevator属性为null或电梯名的乘客的楼层,并将该楼层中满足条件的乘客的elevator属性设置为电梯名(防止其他电梯对这名乘客进行操作)。

关于三种模式,我删除了Morning模式下对于“在一楼等待的乘客到达最大载客量或输入结束时才开始接人”的限制。在Night模式下,为每一部电梯标记好所有需要带走的乘客(保证每一部电梯到达的楼层都是尽量低的)。

同步块分析和锁的选择

这次作业中,我终于把之前一整块的synchronized块切分开了,只对需要对floors进行写操作的部分上锁。

我的线程中只有两个共享对象Floors和WaitQueue。关于锁的选择,由于GetRequest线程读取乘客后存入到Floors中,Elevator线程每次直从 Floors中读取乘客,所以我的线程中都只锁了Floors。由于全局的等待队列WaitQueue的相关写操作都必然伴随着对Floors的写操作,所以只需要一整块代码都锁Floors既可。

调度器设计

电梯的移动和控制电梯进出的策略依然是由电梯内部自行控制的。

由于我不是由一个调度器进行集中调控的,所以我为Person增加了elevator属性,从而保证电梯之间选择乘客时不会发生冲突,从而导致白跑而浪费时间。为了配合这种标记方式,我为电梯增加了一个vip属性记录标记而为上电梯的乘客数目,保证电梯为已标记的乘客留出空间,防止因为捎带导致标记的乘客没有办法上电梯。

架构分析

UML类图

各个类的作用与第一次作业中无太大区别,此处不再赘述。

Class Metrics

由图可见主要是Elevator线程的复杂度较高。

Method Metrics

可以看到主要是Elevator中的方法复杂度较高。nextFloor()中由于三个模式的寻找方法都放在一起,导致方法内if…else过多,并且random中通过循环遍历floors,导致方法的复杂度过高。randomGetRequest()中还是老问题,此处不再重复。其他几处主要是if…else略多。

UML顺序图

第三次作业

要求

多部不同模式的可捎带电梯。

设计思路

保留以前的Elevator类作为A类电梯,从Elevator继承两个子类ElevatorB和ElevatorC作为B类和C类电梯。

由于B类电梯和C类电梯速度较快但存在无法到达的楼层,所以增加了换乘的操作(虽然总感觉好像没有快多少)。

同步块分析和锁的选择

在同步块和锁上对于之前并没有什么区别。

在这一单元的作业中我都没有出现过死锁的现象。主要原因在于我的代码中都只存在对于floors的锁,并且没有synchonrized块嵌套的现象。在这一单元的debug过程中,我的bug基本都是由于调度策略而出现的,总的来说还是十分顺利的。

调度器设计

我的电梯依然是依赖电梯内部的策略行动的。

为了实现电梯的换乘,我在Person类中增加了一个middle属性表示如果乘客需要换乘,将先在哪一楼下电梯(为了减少工作量,我的设计中都只换乘一次,但换乘多次的迭代空间很多),新增一个type类表示目前该乘客能被哪些类电梯移动。在新建一个Person对象时根据起始楼层和目标楼层设定Middle属性。乘客下电梯时如果未到达目的地(middle != toFloor),则调用setMiddle方法重新设置middle,elevator和type。

如果移动距离大于10且起始楼层或目标楼层有一端在C类电梯移动范围内,则由AB类运送一段,C类运送一段。例如乘客起始楼层2,目标楼层14,则初始设置middle为18,type为C。C类电梯将乘客送到18楼,将middle设为14,type设为AB,elevator重新设为null,等待A类电梯将乘客送到14楼。

如果不满足上述条件且移动距离大于8且只有一端的楼层为偶数,则由AC类电梯运送一段,B类电梯运送一段。例如乘客起始楼层为2,目标楼层为11,则初始设置middle为3,type为AC,由AC类电梯将乘客送到3楼,将middle设置为11,type设为B,elevator设为null,等待B类电梯将乘客送到11楼。

由于乘客进入电梯,从WaitQueue中移除后还可能再次加入,所以以inElevator为空,WaitQueue为空且关闭为条件结束进程会导致乘客换乘回到Floors,其他电梯进程却都已经关闭,导致程序无法结束的情况。所以我在WaitQueue中增加了一个属性number用于计数。队列中每加入一个乘客,number加一,每有一个乘客离开电梯时,number减一。(注意乘客在上电梯时从WaitQueue中移除,但是下电梯时计数器才减少)由于换乘时会重新将乘客加入WaitQueue,计数器先减一再加一,等同于没有发生变化。只有当inElevator为空,WaitQueue为空且关闭且number == 0 时电梯进程才会结束。

架构分析

UML类图

ElevatorFactory:根据输入的不同创建不同的电梯线程。

各个类的作用与之前无多少区别,此处不再赘述。

Class Metrics

由图可见主要是电梯线程的复杂度较高。

Method Metrics

可以看到主要是Elevator中的方法复杂度较高。nextFloor()和randomGetRequest()中还是老问题,此处不再重复。need()用了switch且存在for循环遍历Requests导致复杂度过高。其他几处主要是if…else略多。

UML顺序图

可扩展性分析

可以增加更多的电梯种类。只需要继续从Elevator中继承子类并重写。然后通过ElevatorFactory类创建不同种类的电梯。

可以增加更多的电梯换乘策略。

我认为如果使用状态模式或策略模式可能可以增加其可扩展性。

我的程序中的BUG

第一次作业强测没有bug,互测也没有被找到bug。

第二次作业中强测出现了两个bug。互测没有被找到bug。

其中一个bug是因为在乘客进出电梯时vip值的变动有错,导致电梯Night模式下在一次往返中没有送完所有的被标记的乘客。导致在第二次寻找目标楼层和标记乘客时vip值超过了电梯承载上限。导致程序无法正常结束。另外一个bug是电梯超出了楼层的高度,移动到了21楼。计提原因好像是对于direction值的归零有问题(记不清了好像是)。

第三次作业中强测没有出现bug,互测也没有被找到bug。

这一单元作业中没有出现过死锁等关于线程安全的问题,都是电梯内部策略出错的bug。因为我的synchronized都只锁了floors且不会出现嵌套;只有两个共享对象且WaitQueue的写操作都伴随着Floors的写操作。只需要将有Floors的写操作的部分锁住既可。

分析别人的BUG的策略

由于多线程不太知道该怎样debug,直接看代码有太过困难,自己构造样例也没能成功找到bug,写评测机又不知道判定正确性的部分应该怎么完成。所以我这三次作业都没有找到别人作业中的bug。

心得体会

本单元可以说收获良多。从刚开始完全不知道多线程要怎么实现,不知从何下手,到最后一次作业能够实现不同种类电梯,有换乘的调度,可以说是进步巨大。过程中一直看到舍友为死锁而苦恼,但可能是由于方法的原因,我并没有遇到过死锁等线程不安全的问题。感觉这一单元的迭代难度还挺低的,跟上一单元比起来每一次作业的改动都不是很大,始终有着一定的迭代空间。

 

posted @ 2021-04-26 00:43  anglerfish00  阅读(64)  评论(0)    收藏  举报