OO2021-第二单元作业总结

OO2021-第二单元作业总结

 

第一次作业

本次作业的基本目标是模拟单部多线程电梯的运行。

(1)总结分析同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系

进行第一次作业的架构设计时,参考了第三次课上实验的例程代码设计,分为以下六个类实现:Elevator(电梯)、InputThread(输入)、MainClass(主函数)、ProcessingQueue(电梯内部队列)、Scheduler(调度类)、WaitQueue(等待队列)。

其中,Elevator、InputThread、Scheduler这三个类设置了同步块,对WaitQueue加锁。而电梯内部队列ProcessingQueue仅仅存储电梯的内部乘客数组、乘客数量、当前楼层等等,不参与线程同步。(这一点在后文会分析)

具体而言,输入新的请求时,InputThread取得锁,并将新的请求加入等待队列。当等待队列非空时,Elevator取得锁,在等待队列中选择主目标,前往其起点楼层接乘客,并在每个途径楼层判断是否存在上下乘客、开关门的操作。在第一次作业中,由于只有一部电梯,所以调度策略主要写在了Elevator电梯类的三种工作模式(Night、Morning、Random)里,Scheduler调度类相当于“顾客——厨师”模型里的“桌子”,而且是一个傻呆呆的桌子,仅仅起到一个缓冲的作用,没有任何实际调度功能。

(2)总结分析调度器设计,并分析调度器如何与程序中的线程进行交互

第一次作业的调度实际是由Elevator电梯类的三种工作模式方法实现:

Mornin模式:peopleSchedulerMorning()方法

起始状态在1楼接人,每接到一人后等待2s,如果没有新的请求到达或者已经达到乘客数量限制,则把电梯内部队列ProcessingQueue里乘客的最高终点楼层作为目标楼层,沿途把其他乘客放下,到达目标楼层,放下最后一个乘客之后,返回1楼进行下一轮运输。

Night模式:peopleSchedulerNight()方法

起始状态去等待队列WaitQueue里乘客的最高起点楼层,接到乘客后向1楼移动,沿途接到尽可能多的其他乘客,到达1楼后,放下所有电梯内乘客,然后开始下一次运输。

Random模式:peopleSchedulerRandom()方法

当电梯内部队列为空时,通过randomGetTarget()方法运算,如果请求的起点明显分布于电梯当前位置的同一方向(上或下),则找到等待队列WaitQueue里乘客的最远起点楼层作为目标楼层(寻找最远楼层的目的是让电梯走最远的路程去接人,途中尽可能地使用捎带);如果请求的起点混乱地分布于电梯两侧,则将第一个到达等待队列WaitQueue的乘客请求的起点楼层作为目标楼层。

当电梯内部队列非空时,通过findFloor()方法运算,找到电梯内部队列ProcessingQueue里乘客的最远终点楼层作为目标楼层(寻找最远楼层的目的是让电梯走最远的路程去送人,途中尽可能地使用捎带)。

这里的捎带策略是指,电梯的运行方向和该请求的目标方向一致,即电梯的目标楼层和被捎带请求的目标楼层,两者在当前楼层的同一侧。

(3)功能设计与性能设计

简略UML类图如下:

 

这是详细的UML类图:

 

类的数量较少,但是方法很多,造成结构冗杂,连类图都这么的难看······

究其原因,一是可能还保留着一些面向过程的残留思维,二是ProcessingQueue(电梯内部队列)的锅。本来想着WaitQueue(等待队列)、ProcessingQueue(电梯内部队列)分别加锁,后来发现没必要,因为ProcessingQueue(电梯内部队列)的写入请求必然需要从等待队列取出请求,而删除请求也严格地只有在开门之后才会执行,不存在电梯内部的安全问题,所以就没有加锁,但是没有继续优化结构,这样就导致电梯内部队列类和电梯类之间有很多信息沟通函数,使得程序复杂了许多。

UML协作图如下:

 

第一次作业里的调度类相当于一个中转站,实际的处理在Elevator(电梯)中执行。

代码规模图如下:

 

确实是太臃肿了。主要是电梯类的问题,三个调度方法占了大头,原因是没有合理的封装。

复杂度如下:

 

电梯类里方法较多,且有许多循环、判断,所以复杂度较高。

(4)bug分析

本次作业,强测没有被发现bug。

互测被hack了3次,其实是一个bug,因为我在Random模式下,如果当前没有乘客在电梯外排队就去中间楼层10楼等待,但是这一命令与电梯内有人的状态下findFloor()方法发生冲突,导致电梯可能在10楼接到乘客后卡死。后来通过重新设置判断条件解决此问题。宿舍楼一共有7楼,下次坐电梯到中间楼层4楼时,我一定好好反思!

本次作业,没有发现别人的bug。

 

第二次作业

本次作业要求模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯

(1)总结分析同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系

第二次作业基本沿用了第一次的架构,还是分为以下六个类实现:Elevator(电梯)、InputThread(输入)、MainClass(主函数)、ProcessingQueue(电梯内部队列)、Scheduler(调度类)、WaitQueue(等待队列),但是由于加入了“多部电梯”的要求,所以作出了一些改变。

主要变化在于,WaitQueue(等待队列)一共建立了6个对象,一个命名为inputQueue,直接接受InputThread(输入)的输入请求;另外5个加入ArrayList<WaitQueue> waitQueue作为5个电梯队列(3个常用,2个备用)的等待队列。此二者在Scheduler(调度类)里进行交互。至于加电梯的请求,直接在InputThread(输入)里解决。

在这种情况下,InputThread(输入)对inputQueue(输入请求)、ArrayList<WaitQueue> waitQueue(仅在加电梯时使用,激活第4、5个WaitQueue)加锁,Scheduler(调度类)对inputQueueArrayList<WaitQueue> waitQueue加锁(为了二者交互),而Elevator(电梯)仅仅对自己使用的WaitQueue加锁,用于对分配给自己的乘客运输。

(2)总结分析调度器设计,并分析调度器如何与程序中的线程进行交互

这次作业的调度分为两部分,第一步是Scheduler(调度类)里从inputQueueArrayList<WaitQueue> waitQueue分配,即将请求分给各个电梯,第二步是各个电梯处理自己的WaitQueue(等待队列)。由于第二步基本沿用了第一次作业的方法,所以这里只介绍第一步。

这次的Scheduler(调度类)有了自己的实际功能,设置int now = 0int total存储当前的电梯数,每次分配时取ArrayList<WaitQueue> waitQueue中的(now++) % total号WaitQueue,将请求加入其中。这是一种笨拙的均分调度法。

(3)功能设计与性能设计

(简略UML类图和上次一样)

这是详细的UML类图:(属性、方法更多了······主要是新增等待队列数组的处理)

 

UML协作图如下:

 

调度类发挥了实际的作用,进行inputQueueArrayList<WaitQueue> waitQueue的请求数据交流。

代码规模图如下:

 

由于在第一次的基础上直接迭代,所以还是很臃肿。

复杂度如下:

 

有四个类的复杂度较高,主函数可能是因为利用循环的方式创建对象,另外三个则是因为循环、判断的问题。

(4)bug分析

强测有1个bug,互测没有被发现bug。

这个bug是1个REAL_TIME_LIMIT_EXCEED,当Random模式下,大量请求同时到达1楼时,由于等待队列更新后电梯就直接开始运行,所以电梯接到第一个人后会直接离开,造成电梯未满但其他人在1楼等待的情况。

解决办法是接到乘客后通过:

try {
   waitQueue.wait(10);
} catch (InterruptedException e) {
   e.printStackTrace();
}

将等待队列锁交还调度类去更新下一请求。这造成了一定的时间消耗,但是有利于捎带。

本次作业,没有发现别人的bug。

 

第三次作业

本次作业要求模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,以及最重要的——可停靠楼层的不同。

(1)总结分析同步块的设置和锁的选择,并分析锁与同步块中处理语句直接的关系

第三次作业基本沿用了第二次的架构,同步块的设置和锁的选择、锁与同步块中处理语句直接的关系都和第二次相同。

(2)总结分析调度器设计,并分析调度器如何与程序中的线程进行交互

对于第三次作业的A、B、C电梯,通过在电梯类里设置新的种类属性来区分,不同种类的电梯对应不同的最大乘客数和移动速度。

本次作业没有采用换乘策略,所有的乘客请求进入调度类之后根据起点与终点分为不同的直达模式,具体而言,在1-3,18-20之间往来,起点、终点都是奇数,可以搭乘A、B、C电梯;在1-3,18-20之间往来,起点、终点不都是奇数,可以搭乘A、C电梯;不在1-3,18-20之间往来,起点、终点都是奇数,可以搭乘A、B电梯;否则的话,既不在1-3,18-20之间往来,起点、终点也不都是奇数,只能搭乘A电梯。然后,还是设置int now = 0int total存储当前的电梯数,每次分配时取ArrayList<WaitQueue> waitQueue中的(now++) % total号WaitQueue,如果该电梯的种类符合乘客的直达模式,则将请求加入其中,否则进行下一次分配。各个电梯处理自己的WaitQueue(等待队列)还是沿用的老方法。

(3)从功能设计与性能设计的平衡方面,分析和总结第三次作业架构设计的可扩展性

(简略UML类图和上次一样)

这是详细的UML类图:(属性、方法又多了······主要是新增电梯种类ABC的处理)

 

UML协作图如下:

 

代码规模图如下:

 

复杂度如下:

 

有三个类的复杂度较高,还是因为循环、判断的问题。

可扩展性

按照SOLID原则检查

  • 单一功能原则:第三次作业只有一个电梯类,内部通过private String typeprivate int runTimeprivate int totalNum设置三种电梯的种类、运行时间、最大载客量,但是考虑到可停靠楼层的不同这一主要问题通过调度类给不同电梯直接分配请求解决,这所谓的三种电梯没有本质区别,所以也算是勉强做到功能单一了吧。

  • 开闭原则:程序整体框架基本稳定,相比于上次作业,电梯类增加了种类属性后,其他部分改动较小,基本满足了开闭原则。

  • 里氏替换原则:程序中未涉及继承,所以未涉及里氏替换原则。

  • 接口隔离原则:程序中未涉及接口,所以未涉及接口隔离原则。

  • 依赖反转原则:电梯只有一个类,但是其实际运行依照第一次作业里的单一电梯种类实现,ABC三种电梯的更高级具体实现仅仅作用于该类的两个参数,所以勉强算是满足了依赖反转原则,但是实现得并不太好。

可拓展性分析

  • 由于未使用换乘策略,第三次作业未来的可拓展性不太乐观,假设没有可覆盖全部楼层的A类电梯,那么这种寻找直达方案的架构将直接失败。

  • 调度的划分策略是简单的轮询查找,并没有考虑当前电梯运行压力的大小,不利于应对某一类请求大量出现的情况。

(4)bug分析

本次作业,强测、互测没有被发现bug。

互测也没有发现别人的bug······

(5)过程经历

第一版是直接创建了三个独立的电梯类ElevatorA、ElevatorB、ElevatorC,结果发现调度类不知道现在有几个A、几个B、几个C,就放弃了。现在想了想,当时确实犯傻了,如果三个电梯类ElevatorA、ElevatorB、ElevatorC都继承自最初的原始Elevator,逻辑会更清晰,代码实现也更美观。

第二版就是最后实现的思路,只有一个Elevator类,内部通过private String typeprivate int runTimeprivate int totalNum的属性设置实现3种电梯,感觉没有继承写法逻辑清晰。

 

心得体会

  • 线程安全非常重要!!!第一次作业的时候,初学多线程的我面对死锁问题手足无措,当时不知道JProfiler这个工具,所以采用原始的打印debug法,在所有关于锁的操作语句的前、后加上不同的输出语句,然后根据打印的语句顺序、数量,以及自己在纸上模拟的线程交互过程,慢慢找,终于找到了问题所在,十分痛苦。第二、三次作业在第一次已经成功的基础上迭代,而且对多线程逐渐有了些理解,所以没有出现什么严重的线程问题。

  • 层次化设计有助于程序实现。在实验代码的启发下,我首先完成了队列类的实现,之后再完成了依赖队列类的电梯类、调度类、输入类,这大大化简了工作难度,而且也便于架构设计。

  • 一定要多测试!第一次作业的时候,我就隐隐约约意识到设置电梯在Random模式下可以去中间楼层10楼等待的策略可能有问题,但是懒惰的我没有及时测试,结果果然在互测中被大佬打出了这个bug,免不了再花时间去自测、修复。

  • 这次学会了IDEAPlantUML绘图插件的应用以及JProfiler的简单应用(后来没出现大的线程问题,所以用得不多)。

  • 第二单元终于结束了,感觉收获许多,感谢老师、助教的辛苦付出。未来的OO学习,我会继续努力!!!

  •  

posted @ 2021-04-23 16:50  BUAA-Panzer  阅读(102)  评论(3编辑  收藏  举报