可捎带电梯——OO第二弹
OO第二单元——可捎带电梯
零、目录
- 作业分析
- 设计策略分析
- 结构分析
- 可扩展性分析(Homework 7 only)
- bug分析
- hack策略
- 心得体会
一、作业分析
Homework 5
1.设计策略分析
实话说,本次作业对我的挑战还是挺大的,毕竟是第一次接触多线程,在几天之内理解概念、了解同步机制确实耗费了我不小的精力。
- 在了解了关于多线程的一些设计模式之后,我发现本次作业的要求和“生产者-消费者模式”有很大的相似度——输入线程
Input
可作为生产者,向托盘WaitinLine
中实时生产请求,而电梯线程Elevator
则可以作为消费者,从托盘WaitingLine
中实时获得请求。 - 于是,基于“生产者——消费者模式”,我设计了五个类,除去上述的生产者、消费者和托盘外,还有
Passenger
类作为请求,以及主类MainClass
(其实这个主类有点冗余,没有什么实质性的功能,应当和Input
类进行合并)。其中生产者Input
和消费者Elevator
为线程类,而WaitingLine
为共享资源类,我将其封装成了一个线程安全的类(主要用synchronized
实现的,虽然也了解到了ReentrantLock
和各种读写锁,但试了一下感觉性能优化不显著,就懒得大改了……)。
以下为本次作业的UML图,整体来说结构还算比较清晰。

2.结构分析
- 整体来说,这次作业的整体结构就是一个标准的“生产者-消费者模式”框架,所以结构比较简单明了。
- 相对而言,
Elevator
类的内容较多,主要因为其内部封装了ALS可捎带算法,在其内部管理了自己的捎带队列。 WaitingLine
类为共享资源类,内部设计读写共享资源的方法都用synchronized
关键字进行了限制,保证了线程安全。
以下为本次作业的复杂度分析图:


- 可以看到,
Elevator.run()
方法的复杂度非常突出……这里确实需要检讨一下,我把ALS算法的主要逻辑放在了run()
方法中,简单来说就是用if...else...
语句实现了一个有限状态机。
3.bug分析
- 这次作业的逻辑较为简单,我在完成代码后很快就通过了中测,之后自己也没再测出问题,在强测和互测环节中也平稳度过。
(内心:ohhhhhh) - 不过,在本次代码的编写中我也遇到了一个问题,就是如何使得程序安全退出。最后我用的实现方法是,
Input
线程结束后通知WaitingLine
类并进行tag
标记,每次Elevator
内无乘客时对WaitingLine
进行检测,若WaitingLine
内无请求且检测到tag
标记,则结束线程。 - 另一个问题在于,虽然我没有被hack到bug,但性能分爆炸……这一问题一方面是我没有想清楚其他更好的调度算法,另一方面我似乎对题意理解有一些问题,再加上被大家的CPUTLE吓怕了,导致最后调度结果很不理想。
Homework 6
1.设计策略分析
- 本次作业的设计策略基本延续了上一次的做法,而且感觉“生产者-消费者模式”似乎在拥有多生产者或者多消费者的情况下才能发挥出更大的优势。
- 相比上一次作业,本次做出的修改主要在于算法方面,然而事实证明,不太成功……
以下为本次作业的UML图:

2.结构分析
- 从上面的UML图可以清晰地看到,本次作业的结构和上次作业并无太大差异,只有一些微小的调整,比如把
Elevator
线程的创建放在了WaitingLine
中。和上次作业相同,这里的WaitingLine
为一个单例模式类,设计的目的是为了保证线程安全,但其实后来发现好像也没有这个必要,不过本次作业中并没有进行改动。 - 在算法方面,本次作业采用的方法是在
WaitingLine
中设置了五个等待队列链表,然后将所有请求平均分配到各个电梯的等待队列中。事实上,这样的方式可以说是非常愚蠢了……本来ALS可捎带算法就是让电梯能够从当前等待队列中选择方便捎带的请求进行捎带,而我这个操作却完全破坏了这一模式(败笔石锤了)于是,虽然我在Elevator
中改进了ALS算法,但还是于事无补……
以下为本次作业的复杂度分析图:


- emmm……
Elevator.run()
的问题似乎还是没有解决(事实上下次作业也没有解决……),不过这个问题我当时也注意到了,只是我觉得有限状态机的控制逻辑本身就有些麻烦,恐怕封装成另一个方法也只能是拆了东墙补西墙。
3.bug分析
- 本次作业在互测和强测环节依旧没有出现bug
(故作淡定.jpg)。 - 在性能方面,这次作业果然不出所料的创造了历史最低,无bug强测85,估计前难有古人,后也难有来者了。不过这次性能分爆炸倒让我想清楚ALS算法的要点了,对第三次作业也有一定的指导作用。
Homework 7
1.设计策略分析
- 本次作业的设计策略进行了一定的改变,将原来的“生产者-消费者模式”做了一些修改,去除了
WaitingLine
的单例模式,整体上改为了以“Master-Worker”模式为核心框架的结构。 - 这样的改动并没有大幅改变我的代码框架,不过凭空添加的功能却使得单个类的负担变得重了起来,客观上不利于之后的迭代开发。
以下为本次作业的UML图:

- 整体结构上与上次作业变化不大,只不过类之间的交互方式换成了“Master-Worker”模式的方式了。
2.结构分析
这次作业主要在算法方面做出了一定的调整,具体内容如下:
- 首先,我终于意识到了强行均分请求的愚蠢了。于是这次在
WaitingLine
类中只设置了三个等待列表,用于三个类型的电梯进行请求的获取。 - 在具体的请求获取过程中,一个类型的电梯可以有不止一台,这些电梯则根据各自需要选取自己的乘客和捎带队列,这样就可以做到在不破坏ALS捎带机制下尽可能的平均分配请求了。
以下为本次作业的复杂度分析图:


3.可扩展性分析
- 从第一次作业开始,我采取的都是类似于“生产者-托盘-消费者”这样的结构,其中每个部分都用一个类进行了实现。
- 这样的结构优点很明确,就是结构清晰、简单易懂,但是随着要求的不断提升,每个类的内容都会不断增多,这样就会违背SRP(单一原则),从而使得类内部的成分变得复杂。
- 如果单从可扩展性的角度而言,我认为homework7的代码还比较满足OCP原则。因为我的几个类之间耦合度比较低,所以如果需求有变化,只需要改变某个类里面的部分方法即可。如果对于算法上有改进,则直接对
Elevator
进行改动即可。但是,由于我的结构比较简单,单一类实现的功能比较多,所以我满足的OCP多少有点自欺欺人的感觉(因为类很少,所以不会改变很多……)。 - 如果要对现有代码进行重构,我会将现有的单一托盘分为两级,第一级为全局等待队列,第二级为局部队列,前者唯一,后者与电梯数匹配,两级托盘之间可以再添加一个调度类,将原本位于
WaitingLine
和Elevator
中关于调度算法的工作分派出去,从而减轻单一类的职责。从可扩展性的角度而言,这样的代码也更加便于修改。
4.bug分析
这次作业的互测和强测结果还算比较令人满意。
- 在互测和强测中,我依旧没有被hack到bug。
(相较于上一单元两次被hack爆,真的是感动哭了) - 这次作业的强测性能分同样有了较大提升。由于我将原来的无脑平均分操作去除了,并合理的对部分请求做出了拆分操作,这次的性能终于看得过去了。不过这次好像很多同学都拿到了接近满分的成绩,自己仍然有一些差距,感觉应该还是处理捎带过程中的算法调度造成的。
二、hack策略
忍不住感叹一下,多线程的测试真的好复杂好复杂……
- 实话说,这次我在测试方面花的精力相较于第一单元似乎并没有显著提升。主要原因可能是因为自己依旧没有尝试搭建自动化评测机,然后手动测试又太过复杂吧……
- 不过在本单元的作业中,我多次尝试了TDD开发模式,在编写代码前先构建了一些测试数据。事实证明,这样的做法还是很有效果的,虽然本单元我每周开始编写代码的时间点明显晚于上个单元,但代码的编写速度明显得到了提升(除了homework5,过程中研究了好久多线程……),从结果的角度来看,本单元竟然出乎意料的没有被hack到任何bug,令人感动!
(不过也有可能是很多人和我一样,懒得手动hack) - 然而有一点我必须检讨一下,由于本单元经常懒得手动hack,导致我代码的阅读量相比上个单元降低了不少。感觉从同学的代码中还是能学到不少东西的,以后有时间还是应当多看看别人的代码。
- 回到hack策略的话,说句实话,本单元的作业复杂度确实不算高(下限不高,但上限是真的高),所以其实测试方面可以考量的内容也并没有那么的丰富。在homework6的中测阶段,我发现中测样例并没有测试到电梯超载的情况,于是我在测试阶段有针对性的构建了测试样例,也确实成功的hack到了别人。整体来看,三次作业我的做法基本都是在互测阶段挑选了一些自己原来构建的比较特殊的样例进行了测试。
三、心得体会
- 不知不觉,OO课程已经过去了一半,不得不承认,这门课成功的在计组之后刷新了我对于“肝”的认知——四周一个单元,从基础的面向对象思想和Java操作,到从未谋面的多线程,不变的只有ddl……
- 不过与此同时,在这门课程中我的收获确实也很多……好我觉得自己要开始写课程总结了,就此打住,这个还是留到最后吧hhh
- 在本单元的学习中,多线程是我们学习的重点,而其中线程安全和同步控制则一直贯穿始终,也着实给我带来了不小的困扰(特别是每次看着自己的代码一次次跑的结果都不一样的时候)。但同时,在真正理解了多线程之后,一扇大门似乎完全为我打开了,我了解到原来编程也可以从一维扩展到二维(虽然从CPU的角度来看,可能是“伪二维”吧)。这方面的知识显然可以大幅提升我今后编写程序的运行效率,不过有得必有失,高效的背后则是线程安全和同步控制的折磨……好消息是我在这个单元的学习中并没有发生过死锁的现象,希望之后也能多加注意,避免在美妙的多线程中植入这种毁灭性的bug吧。