面向对象第二单元博客作业

0 引语

  在第一单元中,我们初步了解了java语言和面向对象的思想。经过四周的学习,主攻多线程的第二单元也圆满结束,在本单元的学习中,我们通过迭代开发的方法,完成了三次有关电梯调度算法的作业。在第一次作业中(相当于整个课程的第五次),有关多线程的知识还运用不太好,给了我不小的挑战,但好在后续的迭代在找准了思路的情况下,进展相对比较顺利。在本单元的前三周,每次作业依旧是由评测互测bug修复三个环节组成。下面,我将以博客的形式对这一单元的内容加以总结和分析,发表我的看法。

一、程序结构分析,迭代和重构

1.1 第一次作业

  和上个单元一开始和蔼可亲的“第一次作业”不同, 这次的作业由于需要明确采用多线程设计,故对于没有了解过多线程的编程者有一定难度。在这次作业中,“输入”模块随时都可能会向电梯发送新的请求,电梯在接到请求后,需要实时的前往指定楼层接送乘客。这也就自然而然的产生了一个想法:既然请求随时都可能到来,那我让电梯(或电梯的调度器)一直循环判断是否存在新的请求,随时处理请求不就行了?NO!狡猾的课程组早就想到我们会这样做,于是将这种方法定义为“暴力轮询”,限制了CPU时间,当程序的CPU时间超过1.0s时,BOOM!你的程序就宣告失败了!所谓CPU时间,是指程序实际运行代码部分时所花费的时间,sleep和wait的时候是不算的。

  在暴力轮询的办法宣告失败时,我又想出了另一个方法:既然一直循环会导致CPU超时,那我在每次循环后sleep一小会是不是就不超时了呢?很遗憾,我的这种尝试仍旧以失败告终。不管是sleep10ms还是100ms,仍然是CTLE。这也使得我浪费了很多次提交次数。

  最终,我还是老老实实使用sleep-notify的方式,在架构上采用生产者-消费者模式,本次作业我使用的类如下:

  (1)MainClass:很短,由于这次的作业是基于多线程,大部分工作都交给各个线程类的run方法了,Main中只有启动输入和调度器线程的代码。

  (2)Tray:仿照课上的“传送带”制作的类,内含一个队列和两个synchronized方法,用来构建起生产者和消费者的连接,通过一个nullsign标志使得此轨道可以像传输正常元素那样传输null,null元素和线程停止有关,稍后再说。

  (3)Input:专门用来处理输入的线程类,run方法中包含了利用指定输入接口获取PersonRequest的过程。在获取到PersonRequest后,线程将作为生产者把这个请求放入tray中,即使接收到null也会照放不误。

  (4)King国王(不是),掌管所有电梯的大王,拥有调度所有电梯和向所有电梯发送请求的强大能力……个鬼啦!由于第一次作业只有一部电梯,这个King其实名存实亡,仅仅是从input接收信息然后发送到elevator中去而已,它同时作为生产者和消费者,掌管着来自input的Tray和去往电梯的Tray。

  (5)Elevator:电梯的线程类,这次任务的主体,拥有开门、关门、运行等方法和一个电梯内调度的manage方法,执行开关门和运行的过程中都有相应秒数的sleep来保证运行正确。

第一次作业方法复杂度分析:

 第一次作业类复杂度分析:

 

    从复杂度分析结果来看,第一次作业由于设计结构简单,也只有一部电梯,整体看起来不是很复杂,只有电梯类的调度方法manage和运行方法run比较复杂,但这也依然在可以接受的范围内,那么,让我们尽快看看下一次作业吧。

1.2 第二次作业

  第二次作业和第一次作业相比,将电梯数量从1部增加到了多部(根据程序一开始的输入可能有1-5部),同时增加了每部电梯的限乘人数限制(7人)。由于这回电梯增加了人数限制,全部把请求堆到一个电梯里就显得效率低下。我们这次有多部电梯,为了能够充分利用资源,设计一个专门的调度器就变得可行。这次作业在上次作业的基础上,进行的迭代结果如下:

  (1)MainClass:更短。这次把启动调度器的king.start放到Input线程中去了,Main方法只有短短的4行。

  (2)Tray:没有任何变化,线程间信息的传输已经成型,没有理由再对其进行更改。

  (3)Input:除了在开头启动king线程和用getElevatorNum获取电梯的数量之外,基本没有变化,在此不展开讲述。

  (4)King:终于发挥实际用处的“国王”线程,它是一个调度器,在开始的时候根据电梯数目启动相应数目的电梯线程,在接收到新请求时,通过判断各电梯人数和所在楼层来选择将请求放到哪一个电梯线程中去。

  (5)Elevator:电梯的线程类,事实上除了增加人数限制之外,并未更改太多的内容,对电梯内部的调度算法manage进行了优化,电梯内外都有请求时综合两方请求和电梯当前人数进行调度,使其针对某些特定的数据能够不出现bug。强测一般测不出来此bug,修改主要是为了防止互测被炸。

第二次作业方法复杂度分析:

  第二次作业类复杂度分析:

  从分析结果看,这次的复杂度相比第一次作业略微增加,实际上是电梯的manage和run需要考虑电梯人数因素,而对于复杂的功能又没有拆分出新的类,所以最终产生了这样的结果。其实从复杂度上看,最好的结果自然是不出现红色字体,有些复杂类能拆分还是尽量拆分成各个独立的功能比较好。但是我为了避免各种奇奇怪怪的改动出现新的玄学bug,最后就没有修改。

1.3 第三次作业

   第三次作业加入了电梯类型的区别,有A、B、C三类电梯,它们最主要的区别是运行的区间不同,A类电梯可以负责其它电梯无法抵达的地下和高层,B类电梯可以负责位于中间的大部分楼层,C类电梯虽然很弱但唯独可以去其它电梯都去不了的3层,三种电梯的速度和限乘人数也略有差别。另外,本次作业还新增了“运行过程中可以随时增加电梯”的要求,可以通过指令把原有的3部电梯增加到最多6部,这就要求调度器采用更灵活的调度策略。

  换乘也是本次作业的一大难题,比方说,一位乘客要从2层到3层,由于没有合适的电梯能够直达(2层只有B类电梯能到,而3层是C类电梯的专属楼层),需要为这样的乘客安排换乘。先由B类电梯从2层送到1层,然后用C类电梯将乘客从1层送到3层。换乘的方法是任意的,你也可以选择2-B-5-C-3的策略,不过效率就要打些折扣。所以2层到3层的乘客为何不直接走楼梯……

  本次作业我使用的类如下:

  (1)MainClass:和第二次作业完全相同,一个字儿都没动。

  (2)Tray:将PersonRequest的队列改为Passenger的队列,稍微改变了null的储存方式,使得它能一次放入多个null(最后用没用上就不好说了)。

  (3)Passenger:这次作业新增的一个类,相当于是一个自定义的PersonRequest。我发觉PersonRequest不能更改它的起始和目的楼层,有些不好用,于是自己创建了这样一个和PersonRequest类似的类,其中记录了起始楼层、中转楼层(如果有,否则为0)、目的楼层。这个Passenger的起始楼层为0时可以传输具有特殊意义的信号。

  (3)Input:新增了创建电梯功能,调用King的newElevator方法创建电梯,但在一个线程里调用另一个线程的方法其实是不安全的,请大家不要学我。

  (4)King:调度器。新增了chooseElevator方法和换乘调度功能,能根据到来的每一个请求选择合适的电梯执行任务,没有电梯能够执行任务时使用合适的换乘策略,有一个权重计算方法来衡量多个电梯时应该选用何种调度策略,是本次优化算法的重点。拥有独特的停止机制:当外部的输入已经终止时,向所有电梯发送询问信号。接收到所有电梯的应答后,若没有新的请求产生,向所有电梯发送停止信号,否则重新发送询问信号。

  (5)Elevator:电梯线程。比起第二次作业有更多的参数(类型、运行速度等),另外,之前的作业中电梯总是消费者,这一次因为涉及到换乘,电梯要把已经完成前半部分换乘请求的乘客送出去并生成新的请求,所以它这次也作为生产者使用。当接收到来自调度器的询问信号时,若工作结束则反馈相应的应答信号。

第三次作业方法复杂度分析:

 

第三次作业类复杂度分析:

    可以看出,这次的程序本身有一定的难度,所以复杂程度较高。这也是没有办法的事情嘛(摊手)。可以看出,被标红的复杂方法都是涉及到运行或调度的boss级方法,主要包括电梯的run、manage方法、国王(调度器)的run方法和用来调度电梯和管理换乘的chooseElevator方法。综合考虑这三次作业,我在面向对象的路上已经行走的比较好,多线程的协同和同步控制也能够顺利完成。

  另外,有关换乘的问题我发过一篇讨论区帖子,虽然讨论区有三个换乘相关的帖子,但我的帖子可是“关注”数最高的哟

  这里是帖子链接:https://course.buaaoo.top/assignment/164/discussion/591

  这里是换乘总结表格:

二、强测、互测与bug分析

2.1 第一次作业

  第一次作业我并没有在强测中出错或在互测中被hack,在性能上也获得了还算不错的分数。

  事实上,我们整个房间没有一个人hack到别人,大家和平共处,平安无事。(其实呢,大家也试图“攻击”了,只是谁都没攻击成功)

  在自行评测过程中,我发现程序主要的问题有:

  (1)千万不要试图轮询!轮询一时爽,超时火葬场。

  (2)线程安全问题!线程安全问题!线程安全问题!重要的事情说三遍。忽略了线程安全问题,要么是在中测中被随机出现的错误搞得一头雾水,要么是侥幸过了中测,然后在强测和互测中被虐出翔。

  (3)电梯的不合法运行问题,如跳层,开门的情况下移动等。

2.2 第二次作业

  第二次作业,我依旧没有在强测中出错或在互测中被hack,在性能上也依旧获得了不错的分数。优化后的算法使得我的性能分比第一次还要高(不过其实性能分也受平均运行时间影响,在这方面单纯的分数其实是没有可比性的)

  在这一次作业容易出现的bug有:

  (1)注意调度器对各个电梯的调度,处理不好的话要么出现乘客“没人要”,要么出现两个电梯抢一个乘客导致乘客分身。说到底还是线程之间的协作和线程安全问题。

  (2)注意电梯的最大人数!任何时候都不能超过最大人数,即使是上下乘客的时候也是如此。所以我们要先下后上(实际电梯中也是这么做的),避免出现超载。

  (3)尽管你大可扬了性能分,只由一部电梯进行调度或采用傻瓜式的调度方法,但是太烂的性能仍旧会导致强测RTLE超时或互测被hack。本次作业我们房间里就有人用大量的1-16层请求,成功hack到了一些超时错误,只可惜hack到数据的人不是我,我在互测之中继续扮演了一轮“打酱油的”。

2.3 第三次作业

  第三次作业的强测我也是All Clear,性能分也非常的好(不过据说这次性能分给分普遍高,嘛不管了)。

  至于互测……什么?有人打我了?我要让你加倍奉还!

 

   简单来说就是我被hack了一处错误,同时hack到了别人的两处错误啦。这两个错误还是我用同一个数据“一串二”的,别的数据都并没有什么用。毕竟互测嘛,Sometimes Naive是再正常不过的现象。

  (1)Hack我的数据长这样:(什么,居然是RE?稀罕稀罕)

这个错误我在修复的时候没有经过任何改动提交就直接过了,怀疑是因线程安全导致的小概率事件。

  (2)我Hack别人的数据长这样:(我精心挑选的数据终于起作用了!)

  这个数据,巧妙就巧妙在:它首先在2.3s处随便搞了个请求把原有的一部A电梯弄到高处,并满足了第一条输出在1.0s-5.0s的规定。接下来直到23.3s才有下一条请求(这是为了增加数据超时的可能性),还没等电梯缓过神来,33.3s处就一下子来了大量请求,全部要用A电梯。然后,它在最后新增了三部A电梯,如果新增的电梯得不到使用的话,那么我测试过,几乎是必超时的。对于提前指定列表式的算法给予致命一击。另外,这些请求的楼层也由15层-20层不等,这是为了尽量增加电梯开门的次数,并且每次接人几乎都能接到不同楼层的乘客,如果调度算法写的不好,电梯将频繁往返于地下和高层之间,超时的可能性就更高了。

2.4 实际Hack情况

  在第一次作业中,我因被Hack扣分0分(0个错误),通过Hack别人得到0分,不亏不赚

  在第二次作业中,我因被Hack扣分0分(0个错误),通过Hack别人得到0分,不亏不赚

  在第三次作业中,我因被Hack扣分1分(1个错误),通过Hack别人得到3.75分,赚了2.75分

  1个bug扣1分,1个hack最高奖3分,正和游戏,稳赚不赔啊。只要认真搞互测,认真bug修复,分数什么的不就有了嘛。

  好了,就说这么多吧,第二单元的作业已经告一段落,接下来还有新的挑战——第三单元,让我们满怀热情,准备下一单元的学习吧!

原文地址:https://www.cnblogs.com/yuanshaohan/p/OO_BlogHomeWork_Unit2.html