OO第二单元总结

第五次作业分析

第五次作业是多线程单元的第一次作业,总体难度不大,对多线程的使用要求也较低,我们只需要设计一个输入线程,分发线程(scheduler),和电梯线程即可完成任务。

而为了能够使三者配合起来完成任务,我们需要利用到一个请求队列作为共享对象。基本思路是一个电梯对应的一个他需要处理的队列,电梯也即消费者,而scheduler就是生产者,不断的向这个队列投入新的请求,即可完成。而输入线程和scheduler间也共享一个队列,scheduler此时作为消费者,输入线程作为生产者,scheduler从中取出请求再投给电梯。

第一次的作业还算简单,涉及到的多线程的知识主要就是waitnotifyAll,我那时候对于多线程还不是太了解,参考示例代码给共享对象的每个方法都加了锁,并且在结尾都写了notifyAll,这种方法确实能够避免死锁的问题,但是又造成了活锁 ,直接导致了我第七次作业出现了轮询的情况,导致cpu超时,具体后续再讨论。

第六次作业分析

第六次作业难度也不大,加入了横向电梯唯一的区别就是多了特殊的building罢了,因为这时候横向与纵向电梯完全独立。

为了实现一栋楼或者一层有多个电梯,我实现了两个类bulidingfloor,这两个类里面装的都是对应楼层的所有电梯的队列。在这里,我让一个电梯就对应一个队列,也就是采取平均分配的方式。Scheduler会平均的将请求投给bulidingfloor里的队列,而对应的电梯就会开始工作。

自此整个第二单元还是非常友好的,第六次作业的代码量也不会特别大,只是增加了bulidingfloor两个类来管理队列,而横向电梯的实现基本和纵向电梯一致,只是部分细节上不一样。(我由于纵向电梯采取主请求策略,而横向电梯采取类似look策略,所以两种电梯的代码时分开编写的。但两者有一定的代码重合,或许通过继承或者抽象是更好的处理方式)

第七次作业分析

第七次作业本应该轻轻松松的,但由于自己手贱想优化,搞得最后强测都WA烂了。

先说第七次作业思路,其实换乘在我们的设计里采取了静态规划的方法,所以变得非常简单,完全不需要改动已有的代码。在过去的代码里我一直都是直接使用PersonRequest类,但在第七次作业里直接使用PersonRequest类显然是不够用的,他无法提高足够的信心。但是我又不想改以前的代码,所以这里充分利用java中继承的特性,我自己编写了一个MyRequest类来继承PersonRequest类,通过重写getFloorgetBuliding等方法来实现不修改原有的电梯逻辑,便能获取到正确的请求状态(目前在那个楼层,下一个目的地在哪)

MyRequest类的实现思路如下,总的而言也非常简单

由于我们采取的是静态规划,所以一个请求最多分三步执行——纵向-横向-纵向,其中transferFloor表示了需要中转的楼层,stage表示当前执行到了三步中的哪一步,maxStep表示做多需要执行的步数。

之后我们重写PersonRequest中的方法。

这里只列出其中两个方法,其他类似。通过这样的继承,在电梯类里依旧是通过PersonRequest进行引用,但是方法已经被重写,电梯只能看到当前请求需要去的楼层以及所在的楼层,这和第六次的逻辑就完全一样了。唯一不同的地方就是我们需要在电梯里将还没有完成所以步骤的请求重新投出去到等待队列,等待调度器的下一次调度。其中finishOneStage就是完成了一个步骤,getToFloor等方法也会因此改变。

自此第七次作业就差不多完成了,只剩下一点调度器的编写,由于采取的是静态规划,所以思路也比较简单,总的而言就是判断最近的楼层且这一楼层请求最少作为中转楼层即可,然后将其投入到对应的floorbuilding即可。

bug分析

本来三次作业轻松愉快的就结束了,也不会遇到什么bug,结果为了优化整自由竞争搞出了bug,最后强测亏麻了。

前面说到我学习示例代码在每个共享对象的方法后面都加入了notifyAll,这在前两次作业中由于采取的是平均分配,一个电梯对应一个队列,显然没有关系,但是在第七次作业中我改用了自由竞争,这就会产生较为隐蔽的bug。

通过上面例子,elevator7和elevator1共享一个队列自由竞争,显然是elevator7先抢到了请求,elevator1因为队列里没有了请求也就应该wait住。从输出结果来看,elevator1确实没有运行了(没输出Arrive),但它却仍在running这是为什么?这是因为elevator1本来应该在wait状态,elevator7在运行。在elevator7运行的过程中它会去查询队列,这个过程会调用一些方法,如果所有方法都写了notifyAll这就会导致活锁,elevator1不会真正的停住,而会被唤醒。

所以,我们只能在addReqsetEnd两个方法中使用notifyAll来唤醒等待着的电梯!这样才能保证真正的锁上。可惜当时没注意到这个问题,也没能掌握print debug法

第二个bug也是优化的过程中失误了,没有做足够的测试,导致会把横向请求丢给不能在这个楼层停的电梯。

UML协作图

心得体会

线程安全

线程安全是初学常出现的问题,出于稳妥我将可能出现安全问题的方法都直接采用了synchronized关键字,为了避免死锁都加了notifyAll。然而在经历了第三年次作业的bug后我对锁有了更深的了解。为了维护线程安全我们不应该这么暴力的使用锁,应该在充分考虑后,恰如其分的在适当的地方使用锁。过于频繁或者过少的使用锁与同步块中处理语句,都会带来意想不到的bug。为了解决这些bug,直接将run方法中添加一些输出语句,是最快最直观的debug手段,因为静态使用ide的debug很多时候无法复现bug。

层次化设计

这次的架构还是较为满意,第一次的架构直接使用到了第三次,并且通过继承的方法极大的减轻了工作量。其中scheduler相当于一级调度器,building和floor相当于二级调度器。通过这个单元的学习我对java语言特性有了更多的了解,它的这些特性使得在架构设计上更讲究,即使面对增量开发也无需大规模重构删改,我们所做的只是位新需求增加一部分代码,原有的代码无虚改动,不仅可以减少编写代码难度,也能减少测试难度。(比如我第七次作业中采取的MyRequest类)

posted @ 2022-04-30 17:12  马曦迪  阅读(36)  评论(0编辑  收藏  举报