OO第二单元总结

从多线程谈作业的设计策略

​ 这一部分主要谈一下我的作业中涉及到多线程的地方。由于我的作业是依次迭代,整体架构没有大的变化,所以我以最复杂的第三次作业为例。

​ 首先是要确定哪些类要作为独立的线程,这一点非常重要,会影响之后所有的架构涉及与实现和优化的复杂程度。首先main作为主线程是必须的;还有就是电梯,每个电梯要作为独立的线程,我认为也是没有什么疑问的。剩下的就需要仔细考量,调度器需要作为独立的线程吗?调度器作为独立线程肯定是一种合理的设计,老师在总结课上也给出了一些设计方式,但我一开始就放弃了这种想法。因为多一个(一类)线程毫无疑问会给程序增加复杂性,给程序保证正确性和鲁棒性增加困难,更重要的是,调度器作为一个独立的线程似乎并没有明显的优势(比如你使用动态的调度策略,虽然比静态调度复杂的多,但是有明显好处的),而且我并不想把太多的职责放在调度器上,希望简化调度器的设计,所以我最终选择了将调度器不作为线程。此外,还可能有一些独特的设计会引入别的类,由于我没有涉及到,就不过多探讨了。

​ 其次是多线程的同步控制方面,我在这方面并没有遇到太多的坑,仔细分析找到所有共享资源加锁即可。唯一遇到的一个bug是在初期涉及时偷懒想使用官方库中的线程安全容器,但犯了糊涂,同时用了两个容器,虽然每一个都是线程安全的,但整体却出了问题,后来老老实实都使用synchronized就没有出过线程安全的问题。

​ 接下来讨论一下多线程的协同,这就需要架构设计了。首先是共享资源的位置。最容易想到的架构就是一个控制器加上电梯类,那么等待处理的请求应该放在电梯,还是控制器,或者一个单独的类,这需要在一开始想好。我的做法是把等待的请求独立出来,而且不是一个单独的队列,也不是为每个电梯设置一个队列,而是为每个楼层设置一个队列,更进一步说就是增加一个楼层类,这可能是给我带来最大好处的设计,让三次作业都非常顺利完成。接下来说一下这么做的好处。

​ 楼层类最直接的好处就是降低调度器与电梯之间的耦合。增加这样一个中间抽象层,类似于生产者消费者模式,电梯从在到达某一层楼时,从相应楼层获取请求;当有新请求时,由调度器放入相应楼层(或者将任务拆分后放入相应楼层)。其次,可以减少电梯运行策略对调度器的依赖。我的策略是让电梯稍微有一点“智能”,实在没办法确定下一步时,再向调度器请求调度。这使得我的调度无比简单,而最终效果却出奇的好,因为让每一个电梯自己决定一些简单的运行策略,实际上是给调度增加了一些随机性,让整体性能在面对不同的输入时都还能保证不太差的结果。

​ 另外一个容易出错的点是如何保证所有线程正常关闭。我的做法是当输入结束的命令时,会将调度器的一个结束的标志位置为true,而调度器在受到调度请求时,如果发现标志位为true,且没有没有完成的请求,就向电梯发送结束的指令而不是调度的指令,电梯就会正常结束。

​ 最后说一下我的电梯的运行策略,前面提到我的电梯是自身具有简单的调度能力,调度的方式我从第一次作业保持到了第三次作业,都取得了还算不错的成绩,我觉得还是很成功的。首先电梯在有人乘坐时,会优先把所有乘客送至目的地,在途中经过的楼层如果有合适的乘客也会载入,没有采用指导书的策略去判断是否可以捎带,而是简单粗暴的都载入,简化了程序的实现,最终性能也还不错。如果没有乘客时,会考察当前运行方向上是否有楼层上有乘客在等待,如果有的话会直接去,再无限循环上述过程。如果没有乘客,且当前方向上没有正在等待的请求,才会向调度器询问,并在询问这一个方法上阻塞,实际效果是,如果调度器发现合适的请求,就会及时通知该电梯去相应楼层,如果没有请求,调度器的该方法会wait直到新的请求,由此实现的电梯在没有工作时的暂停,如果调度器返回了停止,电梯的运行就会结束。

各次作业的分析

第一次作业

类图与复杂度分析

​ 第一次作业是实现一个可稍带的电梯。这道题对于刚结束多线程的同学可能难度是很大的(个人认为远大于后两次作业的迭代),但它实现的功能又太重要了,如果做好,以我的经验后两次作业只要做很简单的拓展就能取得还凑合的成绩。

​ 复杂度分析同上一次博客,也是只列出了存在问题的地方。首先从类图上看,这次作业几乎没有设计继承与接口,每个类完成相对独立的工作。从复杂度分析看,有2个方法存在复杂度过高的情况,一个是电梯类的run方法,这是由于我把电梯运行的简单调度策略放在了run里,当时的考虑是因为简单,只要二三十行的代码,但回过头来看,为每一个电梯设置一个独立的子调度器显然是更好的设计;另一个方法是调度器类内的goTo,这个方法是用于电梯向调度器请求调度,复杂度过高可能是包含了太多的条件判断,但我并没有想到如何简化这一部分。

SOLID原则的体现

单一职责原则:总体还是体现了单一职责原则,特别是楼层类的加入进一步分离了类的功能,唯一的失误可能就是没有设计一个子调度器,把电梯的run方法中的一些内容独立出去。

开放封闭原则:这一点做的还是不够好,首先是楼层中的请求直接使用了官方包中的PersonRequest,导致第三次作业时进行了重构,改为了自己的请求类;还有就是电梯中的一些参数写死了,如某一操作的时间等,后续区分不同的电梯时改为了可设置的,回过头来看,使用一个抽象电梯类可能是更好的设计。

里氏替换原则:没有使用继承。

接口分离原则:没有使用抽象类和接口。

依赖倒置原则:做的不够好,同上面的叙述,电梯应该有一个抽象类,调度器甚至也可以考虑使用一个抽象类,方便对不同的调度规则进行替换。

第二次作业

类图与复杂度分析

​ 第二次作业要求实现多部可稍带电梯,同时限制了电梯最大人数,增加了负数楼层。我的改动也比较小,电梯运行完全继承了上次的代码,只不过一开始初始化了多部电梯,最大的改动是负数楼层的添加。因为我的楼层类一开始是使用ArrayList进行存放。一开始我考虑使用HashTable实现楼层号到楼层的映射,但这么做要改的东西太多了,而且从性能角度讲,使用哈希表也没太大必要,静态数组显然更适合。因此我结合了操作系统中增加一层抽象的方式,在楼层类中简单实现了数组下标到楼层号以及楼层号到数组下标的映射函数,非常简单的解决了这个问题,减少了对之前代码的改动。

​ 由于架构几乎没变,所以类图和复杂度分析和第一次作业几乎完全一致,就不再赘述了。

SOLID原则的体现

​ 同第一次作业。

第三次作业

类图与复杂度分析

​ 第三次作业是最复杂的一次,增加了对电梯的分类,不同种类电梯可到达的楼层以及运行速度等参数不太一样,同时增加了在运行中启动一台电梯的命令。

​ 最大的困难是限制了不同电梯可到达的层数,导致某些请求必须又多个电梯接力完成,之前的简单调度似乎不再可行。我选择的方式是静态调度,最大的原因是简单,而且动态调度可能需要考虑非常多的参数(如其他电梯的运行状况),才能做到不错的效果,简单的利用图计算最短路显然是不合理的(和静态调度也没什么区别,如果没有缓存甚至会浪费计算资源),因为简单计算出的”最优路径“如果不考虑当时所有电梯的运行情况,可能导致过长的等待,而如何调整不同的参数权重以达到优秀的调度,我并没有信心做的很好。

​ 因此我的静态调度策略是在请求输入时,就查表把该请求拆分成不同的请求,并发送到相应的楼层;之前的单个请求变成的一个请求队列,只要把之前对单个请求的处理改成对队列头元素的处理即可,要改动的代码很少,原有的调度也可以得到兼容,虽然和最优秀的调度比要差的多,但从最终分数看,还是有97分左右。考虑到我只花了三四个小时简单的在上次作业的基础上进行迭代(一大半时间还是在解决电梯不能正确停止的bug,因为出现了请求的拆分,之前的停止判断条件出现了问题),可以说是性价比相当高了。

​ 从类图上看,我只是在上一次作业的基础上重新封装了请求类和安全输出的类,其他的沿用了第一次作业的设计,至少说明第一次作业的整体设计还是比较成功的。从复杂度分析上看,首先时请求类的构造函数复杂度爆炸,这一点之前就预料到了,因为偷懒把请求的拆分全部放在了构造函数中进行,确实不合理;其次是请求拆分时,我实际上为了简化,规定了子请求应该被哪一类电梯相应(静态的不能更静态的调度方式),导致具体请求的查询函数等也出现了复杂度过高的情况,因为要找一个可以被响应的请求,这主要是因为想尽可能少改之前的代码,说明第一次作业的可拓展性还是有待提高。

SOLID原则的体现

单一职责原则:新拓展的类体现的并不好,因为请求的拆分交给了Request类本身,导致Request类有些复杂,可能交给调度器类会更加合理。

开放封闭原则:体现的不好,同样是上述问题,导致Request类拓展性很差。

里氏替换原则:没有使用继承。

接口分离原则:没有使用抽象类和接口。

依赖倒置原则:做的不够好,实现一个抽象的Request类更利于后续拓展。

Bug分析

三次作业我在强测和互测中都没有被发现bug。

以下是我认为可能比较容易出错的地方:

  • 锁的添加,特别是要注意保证操作的原子性,如果你同时用了两个容器,两个容器分别保证线程安全还是可能出问题。
  • 线程的结束。分析清楚结束的判断条件。

体会与感想

​ 这一单元深入学习了多线程和相关的设计模式,同时加深了对面向对象思想的理解,自己的编码能力也得到了很大的提升。

posted @ 2020-04-17 12:16  max2333  阅读(143)  评论(0)    收藏  举报