面向对象第二单元作业总结与反思

面向对象第二单元作业总结与反思

前言:

第二单元作业也结束了,到此为止OO课算是完成了一半。本单元是整个课程较难的一部分,当然相对地也有很多收获,需要好好总结。首先我会介绍这一单元作业的总体设计思路,对于每次作业,阐述具体的架构与代码实现,解释每个类的设计考虑,并展示整个项目的类图与时序图。之后对我自己每次作业出现的bug与hack的别人的bug作简要说明。最后是对于本单元作业的自我点评与心得体会


1.总体架构设计

本单元主要考察多线程程序的设计,重点关注线程设计,同步块和锁的选择,以及各个线程之间的调度。

1.1 线程设计:

本单元作业是实现一个电梯系统,可以接受乘客请求并将乘客正确送达。由于问题中存在并发模式,所以需要设计多线程程序。首先确定将哪些部分设计成一个线程,结合老师在课堂上讲的内容以及对作业的思考,我选择了将“请求”和“电梯”当做线程的方案,将调度器设置为共享对象。原因是两个线程的设计相对简单,就是生产者消费者,再加上一个共享对象。

1.2 同步块与锁

前文提到将调度器设置为共享对象,实际上在写程序的时候肯定不能将整个调度器类都锁上,真正上锁的是候乘表类。我的候乘表类包含两部分:等待表和运行表。等待表接收输入线程输入的请求,并在电梯到达时将对应的请求送入运行队列。运行队列接收等待队列的请求,并在到达目的地后弹出。

本单元我全部采用的synconized锁,不论读写都要在获得锁的情况下才能执行。

1.3 调度

我将等待与运行队列都放入调度器中,电梯只管运行和输出,所有与请求有关的操作也都在调度类中。这种方式是有弊端的,主要方法都在调度器类中,使得这个类十分臃肿,与别的类的耦合度也很高。第一次作业还好,第二次开始我的调度器类就直接超出代码总量的限制,我又不想重构不得不拆开到好几个文件中。

调度器本身写好一系列方法(比如上下乘客,添加请求)然后由对应的线程调用方法。使用单例模式全局创建唯一的调度器类,然后作为参数传入电梯类和输入类再一次精准踩雷

接下来结合具体作业的情况来说明以上提到的架构设计。


2.作业分析

2.1 第五次作业

实现一个简单的电梯系统,只有五部纵向电梯,无换乘,主要考察最基础的多线程程序设计。

首先封装两个队列用作等待和运行。然后写调度器类,我的思路是先把共享对象写好,这样之后的电梯线程和输入线程就可以直接调用方法了。输入线程中只需要锁住等待队列,电梯线程则是等待运行队列都需要锁,其实就是很基础的生产者消费者模式。

第五次作业由于对锁的机制不太熟悉,我的synconized全都是加在方法上的,后续的作业改用同步块的设计。

调度策略方面,我没有采用指导书的基准策略,而是使用一种类似look的策略,电梯首先确定一个运行方向,比如上行,然后在上行期间只接受上行的请求,运行到顶之后反向,接受下行请求,若此时没有下行请求,就找最下层的上行请求,如果都没有就wait

在本单元的三次作业中,关于线程结束的方式都是一样的,有两个判定条件:

  • 输入停止。对应Stop类的inputEnd.
  • 整个电梯系统中不再有请求,即全部请求都到达目的地。对应Stop类的runEnd.当Stop中的两个变量全为true,则电梯线程停止。

类图:

时序图:

可以看出,第五次作业的架构并不是非常优秀,main承担了一部分Input的功能,代码耦合度很高。当然本次作业的设计也有可取之处,比如将运行队列交给电梯来管理,减轻调度类的压力,调度器只需要将等待队列的请求发送给电梯即可,如何运行由电梯类判断。这样的设计在后面两次作业中会占优势,可惜后面我给“优化”掉了0_o.

这样改的目的,其实还是为了方便后续的迭代。将等待与运行队列放在一起,电梯分离出去单独运行,本质上只是起到控制台输出的作用,所有有关请求的方法与交互都在调度类中。这样后续需要添加电梯,只需稍作修改,主要的工作都集中在调度器类中。

2.2 第六次作业

在五部纵向电梯的基础上添加了横向电梯,且电梯可以手动创建,但乘客依然不会有换乘需求。

本次作业基本沿用了之前的设计,但是需要在上一次的基础上增加横向电梯的请求,且可以根据输入添加新的电梯。在原有的单部电梯look策略的基础上,又引出多部电梯竞争的策略。我在本次作业中采用的是固定分配策略。为每个电梯创建单独的等待队列,在输入线程将请求送入调度器类时直接分配给各个电梯,同时保证尽可能平衡。实话实说这种策略有些吃力不讨好,效率没有自由竞争高,代码实现也更为复杂。

类图如下:

这次依旧是顶层调度器处理一切请求,但是其实对于我的设计来说,电梯内的运行逻辑非常简单。我预设电梯内有一个变量prDir,即请求方向,代表这部电梯在任意时刻运行队列中的请求方向都一致,所以无需判断怎么停,只要运行队列有请求那就跑,没有再根据等待队列考虑转向的事情。

横向电梯和纵向电梯基本一致,在我的代码中甚至电梯类都不需要怎么改,将之前的移动楼层函数move()拆成两个:moveFloor()moveBuilding()即可。唯一需要注意的点就是环形电梯的方向选择以及AE的调度,这里通过Direction()类中的方法来实现一个环形队列。

对于共享对象与锁,本次作业吸收了讨论区大佬的方案,将输出也封装成一个类,并使用单例模式保证操作的原子性,避免时间戳非递减的问题。其他的锁机制与第五次作业相同,都是锁队列,使用代码块的形式避免锁整个对象。由于在电梯调度中选择固定分配,也不存在同楼座电梯相互抢客的情况,算是一点小小的优势。

时序图如下:

对比上一次作业的时序图,可以很直观地看到本次作业基本只修改了调度器,其他基本不变。虽然在代码实现上略有困难,但结构是真的简单。到第七次作业也是同理,只需添加新的策略并更新调度器即可。

2.3 第七次作业

在原有横纵电梯的基础上新增了换乘请求,其实如果前面作业的架构设计够好而本次作业又不太追求性能的话,这个换乘实现起来十分简单,就直接拆请求然后逐个添加进等待队列运行即可。

调度策略方面,首先是电梯调度,本次作业我将纵向电梯的调度方式改为自由竞争,横向电梯仍保留固定分配模式。在自由竞争模式下每个楼座只有一个共享等待队列,需要格外注意锁的情况。其次是人员进出,依旧沿用之前的模式,总调度器调控等待与运行队列。

为了实现换乘,我重新封装了PersonRequest类,保留官方包的所有方法,并新增一个ArrayList<PersonRequest>,将分割过的请求按顺序放入这个队列,该类对外表现的只有列表的第一个元素。当第一个元素到达目的地,在出电梯后弹出,判断该队列是否为空,若空则原请求到达目的地,若非空则就地加入等待队列。这样实现的好处是不需要改动原有的结构(主要是退出线程的逻辑不需要改动),只需在出电梯时加一个判断即可。

为了实现换乘分割请求,我采用图论的dijkstra算法,在matrix类中维护一个50×50的无向图,用电梯的运行时间作为权值,每加一个电梯就添加新的边,求出最短路径存入上述的PersonRequest类中。虽然理论上讲这种方法找出的路径是最短,但是实际实现的时候会比较麻烦,且在换乘次数不多时(比如仅有一次横向换乘)性能并没有提升,甚至会下降,因为时间复杂度较高。最初我一直担心这样做是否过于复杂而甚至强测都不需要多次换乘,后来是研讨课时听老师讲的,大意是:我们需要关注的是策略,而不是如何实现。仔细想想也对,计算机算数的时间相比于电梯以秒计算的运行时间几乎可以忽略,不如锻炼一下直接上点难度吧_复杂度也就图一乐罢了。

类图:

时序图:

第七次作业的时序图可以看出,从宏观的结构上讲我其实只是增加了一个拆请求的部分,其他基本都维持原状。可能在实现代码的时候稍显复杂,但作为一个迭代程序来说这样一层层的添加功能是十分理想的状态,我从第二次到第三次作业,除去研究算法的时间,修改程序大概只花了半天,思路十分明确。


3.bug与hack

3.1 第五次作业

非常遗憾地没有进入互测,原因我也很清楚。由于对于多线程知识掌握的不太好,第五次作业我遇到的最大的问题在于如何结束线程。我设置了一个Stop类,当输入结束且请求全部到达目的地时将stop置为true。然后程序写完就一直死锁停不下来。。。。直到提交最后我也没找到问题。在后续的bug修复时我认真学习了一些线程间交互与停止的机制,最后找到问题所在,其实非常简单,就是在输入线程停止时没有notifyAll,电梯一直卡在waitbug修复时加上一遍就过了。要说可惜吧确实挺可惜,但这也是因为我自己没学好,没什么好讲的。

3.2 第六次作业

这一次成功避开了大多数的坑,强测和互测都没有问题,但是感觉性能分稍低。本次作业的测试其实分为两部分,横向和纵向。如果横向纵向分开来测都没问题,那么合在一起也大概率没问题。对于纵向电梯,我直接下载第五次作业的强测样例,中间添加几个ADD指令就直接开跑,没有太大问题。横向电梯就手工构建一些比较典型的数据,比如AE之间的请求,环形捎带的策略之类。

我自己费尽心思搞了几套合格的测试数据但是互测房一个都没hack到,大家都很棒!

3.3 第七次作业

这一次也没有什么问题,同样强测互测都全通,性能分也提高了。本次作业我自测的时候同样是回归测试,用上一次的样例稍作修改来测这一次的程序。互测就比较勾心斗角了,我这次注意力集中在换乘策略上,虽然最后我自己还是没hack到别人但是有一个人的程序跑出了206秒的非常极限的时间,我猜再把电梯设计的复杂一点应该就超时了,但后续我没再试。

4.心得体会

折磨的多线程单元终于结束了,对于本单元的完成度我自己还是比较满意的。除了最开始的一点点小问题之外,后续不管是架构还是迭代都保持良好的表现,有些遗憾的是我把更多的精力放在如何写代码上,其实有些忽略多线程本身的实现。一些巧妙的设计比如读写锁,线程池等等没有机会实践。说到这里我有一个小建议,下一届在讲多线程这一部分的时候可以多提一提锁与解锁这种逻辑在底层是如何实现的,虽然好像也没有哪能用得上hhh

本单元的作业可以说是项目迭代开发的典型,每一层递进十分清晰,如果最开始的基础打牢了后续添砖加瓦就会显得比较轻松,当然如果要重构也确实比较痛苦。不得不说我自己相对于之前更加重视架构设计了,写程序之前先在纸上好好写一写规划一下真的比什么都强,好过枯坐肝代码不知道多少倍。还有就是我们现在还处于学习阶段,该上难度就上难度,该重构就重构,不要畏首畏尾,冲就完事了!

posted @ 2022-05-03 10:27  苏∴杭  阅读(17)  评论(1编辑  收藏  举报