BUAA_OO_UNIT2 单元总结
第一次作业
设计结构
一共有5个类
MainClass类,负责启动所有的子线程。
Request类,请求类,用于存储请求数据,构建构建共享数据队列。Input类和Schedule类对其进行操作(类似生产者消费者模式)。
Input类,负责读入的类,并且将请求送至共享请求队列。
Elevator类存储了电梯的数据以及一些电梯的操作
Schedule类,调度器,负责控制电梯的运行。
可以看到elevator和request中有个方法复杂度过高
UML类图分析:
类行数分析:
协作图:
调度策略
没有选择传统的的电梯调度算法(毕竟不用考虑乘客感受),选择了纯贪心。电梯有一个主目标,电梯的一切行动都是向着主目标前进。每到一层对主目标进行一次更新,找到离自己最近的请求楼层,并且与电梯里的最近的目标楼层进行比较,选择更近的那一个作为主目标。
自我评价
第一次作业的架构还可以(毕竟题目简单),调度策略也出乎意料的效果还不错,除了个别方法复杂度过高以外,其他都还不错。
BUG分析
互测中被人找到了轮询的bug,说实话,虽然一开始就在指导书上看到了不要暴力轮询的字眼,可是刚接触多线程的我并不清楚这个暴力轮询到底是个啥意思,在后面的中测中也没有出现cpu超时的情况,于是便将暴力轮询的事情抛到脑后。
暴力轮询其实就是生产者消费者模式中消费者在知道库存为0的情况下仍然不停的询问,解决的方法也很简单,当库存为空时进行wait等待生产者唤醒就行。
非常感谢互测中找到我这个bug的同学,不然我很可能在后面付出惨痛的代价。
在线程安全方面,由于本身对多线程掌握不够熟练,所以是严格按照OO课程的教程推荐的方式来加锁的,并未出现多线程安全相关问题。
第二次作业
设计结构
第二次作业在第一次作业的基础上增加了电梯的数量,并且限制了电梯的人数。于是我在第一次作业的基础上增加了一个主调度类,一共六个类,设计结构如下图:
(很碰巧的和后来课程中提供的架构一致)
主调度器负责将主请求队列中的请求分配至每个电梯的请求队列中,而每个电梯的调度器采取的调度策略与第一次作业保持一致,因此工程量较大的地方就是主请求类的搭建。
由于加入了人数的限制,elevatorrun方法修改了之后耦合度过高。
UML类图分析:
行数分析:
协作图:
调度策略
子调度策略与第一次作业一致。
主调度策略:分配新请求时,优先将请求分配给总人数(电梯中人数+自身等待队列人数)少的电梯,若是总人数一致,优先分配给当前楼层离新请求出发楼层近的电梯。
自我评价
分配请求的考虑因素有两个,子电梯人数以及子电梯位置,刚开始不知道该如何权衡这两个参数,最后试了一些数据发现子电梯位置这个参数的影响不太大,于是最终决定优先以人数为准。这种方法肯定不会是最快的,因为考虑的过于简单,没有很好的优化,但是效果肯定不会太差,最后强测的分数也还能看。选择这种方法主要是简单,写起来快,性价比比较高(手动狗头)。
BUG分析
在第一次作业的暴力轮询BUG改了以后,就没什么问题了。第二次作业在从第一次作业迭代升级的过程中也没有遇到什么bug,总得来说写的比较顺利,最后在强测和互测中均未被找到bug。
第三次作业
设计结构
第三次作业加入了电梯请求,电梯分了三种种类,出现了楼层限制,运行速度、最大装载人数也各不相同。
相比于第二次作业,增加了:
ElevatorRequests:电梯请求类,用于存储Input传递的新增电梯请求,并于主调度线程中开启新电梯的调度线程。
NewPersonRequest:由于很多人不能直达,并且有的人只能乘坐特定的电梯,于是继承官方提供的PersonRequest类,新增了一个属性用于记录自身被分配乘坐电梯的类型,主调度器分配时,每个人只能在自己可以乘坐的电梯种类下的几台电梯中选择。
Safeout类:对输出操作封装了一下保证多线程安全。
整体架构和第二次差不太多,只是elevatorrun方法变得耦合度更高了。
UNL类图分析:
行数分析:
协作图:
调度策略
调度策略与第二次作业大体相同,难点在于对请求的拆分。显然,动态拆分请求才是可以达到最高效率的,但是由于我对于动态拆分并没有一个好的想法,于是选择了简单易实现的静态拆分请求(有点类似于打表),对于每种情况进行一个分析,在读入请求的时候就将其拆分成两个请求(或者不拆分)。请求类中除了主请求队列以外加入了一个等待激活队列,拆分过后的请求将第一部分(出发楼层-换乘楼层)丢入主请求队列,将第二部分(换乘楼层-目的楼层)丢入等待激活队列。每当一个人从电梯中出来时,便将等待队列中与自己id相同的请求激活(加入主请求队列,从等待队列中移除)。
自我评价
第三次作业写的时候思路没有前面两次作业那么清晰,写的时候经常出现边写边改的情况,导致最后的代码没有前面两次作业感觉好。在思考拆分策略的时候也想去努力实现动态拆分多挣点性能分,最后实在没有想到一个比较易于实现的动态拆分,于是只好放弃(我太菜了)。不过好在静态拆分的效率也能看(虽然和大佬比差的太远了),实现起来也不太难,也不会出现很多bug。感觉代码还是得想清楚再开始写。
BUG分析
由于加入了一个等待队列,因此线程结束的条件需要一些修改。最开始的时候我没想太清楚,导致出现了线程安全问题,出现了人还没送完线程就结束了的情况。在修复这个bug的时候又不小心出现了暴力轮询的bug,不过好在并不是架构问题,只是一些条件没想清楚,重新写了一下以后就又都解决了。
写的简单的好处就是bug少,在强测互测中均为被找到bug。
第三次作业架构设计的可扩展性分析
个人认为第三次作业的可扩展性其实还不错,比较不好的地方是拆分请求的功能为了方便就直接添加至input类中了,导致拆分请求的可扩展性比较差,并且由于是使用的类似打表的静态拆分,基本是面向数据实现的拆分,所以基本毫无可扩展性,除此之外呢,电梯的架构还是能看,加入一些新的功能或者修改电梯的数据等都还算方便。
SOLID分析:
单一责任原则:Input类做的较差,其他的类还行。
开闭原则:实现的一般,部分功能还是需要修改而不是新建
里氏替换原则:NewPersonRequests类做的较好,其他的类未使用继承。
接口隔离原则:实现的还行。
依赖倒置原则:涉及较少。
寻找别人BUG策略
丢给自动测评机跑,并且使用一些本地记录下的测试自己代码过程中出现过问题的数据进行盲刀,有时候有意外的惊喜。
心得体会
不得不说,从根本不知道什么是多线程,到现在能够写出一个能运行的电梯调度,从最开始的看见synchronized就头疼,根本不知道wait、notify、sleep是什么,到后来还能进行一些多线程的debug,这之间的进步还是相当巨大的。经过这个单元的作业,再加上oo、os理论课对多线程的高强度教学,不能说掌握多线程了吧,但是确实对多线程有了一定的理解,可以理解一些看起来“莫名其妙”的bug。
作为一个之前完全没有java基础的人,每一次的单元作业写起来其实都挺艰难的,毕竟不仅要学新知识,还要快速上手实现代码,还要能保证代码正确性,确实是一件不容易的事情。相比于第一单元的作业,这一单元的作业OO味儿就要浓了许多,从第一次作业到第三次作业的过度自然了许多,没有出现重构的情况。
希望下一次作业也能顺利完成。