面向对象第二单元总结

面向对象第二单元总结

第一次作业

(一) UML类图和UML顺序图

  • 第一次作业的大概思路是采用生产者-消费者模式,InputHandler是生产者,Elevator是消费者,Dispatcher作为托盘,实现线程交互。

  • 调度策略采用课程组推荐的ALS。

  • 就可拓展性而言,本次作业的可拓展性还是ok的,采用Dispatcher把线程的交互独立出来,让电梯只做电梯该做的事,事实证明之后的两次作业对电梯的修改还是较少的。欠缺的地方在于电梯的一些参数直接代的常值(如移动速度,容量等),没有考虑到有新增不一样类型的电梯的需求。

(二)同步块的设置和锁的选择

  • 如上文所说因采用生产者-消费者模式,因此只需要对托盘(本例中为dispatcher)实现线程交互的地方上锁即可,dispatcher在本次作业中是单例构造的,所以直接锁了这些对requests操作的方法。

  • 例:

    public synchronized void addRequest(Request request) {
        requests.add(request);
    	notifyAll();
    }
    

(三)调度器设计

通过单例构造的dispatcher,主要作用有两个,一个是接收InputHandler的请求并在合适的时机将请求加入电梯的passengers队列,另外一个就是接收InputHandler结束信号并通知电梯线程结束(可以参考时序图)。

(四)Bug分析

本次作业未在公测和互测中出现Bug。


第二次作业

(一) UML类图和UML顺序图

  • 总体架构和第一次类似,就加了一个Controller作为总的控制器,管理多电梯线程。
  • 开始的设计本来是每个电梯配一个dispatcher,由总控制器Controller收到请求后根据当前电梯的位置和移动方向等派发请求到相应电梯的dispatcher里的请求队列,后来看学长的博客发现了一种抢人的策略,即还是只有一个总的请求队列,哪个电梯抢到就归哪个电梯,这样确实会比较浪费电梯资源,常常出现其他电梯“陪跑”的情况,但经过实现验证这个方法反而比上述的分配策略的性能更为优越,而且实现起来也就是半个小时的事
  • 这样的设计只用一个dispatcher即可,即实现总的请求队列的调度。
  • 就可拓展性而言,本次的拓展性感觉是有所欠缺的,主要是只配了一个总的dispatcher不太合理,当第三次作业增加不同类型的电梯时并采用新的分配策略时还是需要每个电梯配一个dispatcher的。

(二)同步块的设置和锁的选择

  • 单例构造的Controller和dispatcher,线程的交互还是体现在dispatcher对请求队列的各种操作上(抢人当然会多线程访问请求队列),因此还是对dispatcher涉及requests的方法进行上锁,总的来说和第一次作业基本上是一模一样。

(三)调度器设计

上面对Controller和dispatcher已经介绍的比较详细了,这里就不多说了。

(四)Bug分析

本次未在公测和互测中出现Bug。


第三次作业

(一) UML类图和UML顺序图

  • 第三次作业感觉是前两次作业数倍的工作量o.o,类还是那几个类,但针对换乘我采用了根据当前电梯负载分配的算法(当然能直达的尽量直达),当电梯负载相差太大时再考虑换乘。
  • 还有就是结束策略的考虑,不只是要InputEnd,还要等到其他电梯都没有请求时(可能会换乘到其他电梯)才能结束电梯线程。
  • 就可拓展性方面而言,本次作业可拓展性笔者认为还是不错的,每个电梯独立的dispatcher、电梯的参数控制等都相对第二次作业都有了对应的实现,线程的交互也尽量的集中到了Controller中,可以说是将各个模块的功能都独立起来,降低了代码的耦合性。

(二)同步块的设置和锁的选择

  • 本次作业还是InputHandler和电梯这两个线程类,线程的交互体现在Controller对请求的拆分和下发,以及电梯状态的读取,而dispatcher只由对应的电梯访问,所以上锁的主要是Controller中涉及请求和电梯状态的方法。
  • Controller作为单例构造,在本次作业中类似生产者-消费者模式中的托盘,synchronized对应方法即可。

(三)调度器设计

  • 一个总的Controller和每个电梯配的dispatcher。
  • 总的Controller接受InputHandler下发的请求以及电梯上传的换乘后半部分请求,通过上文提到根据当前电梯负载量的算法拆分请求(换乘就拆分,不换乘不拆)分配到对应电梯的dispatcher进行处理。
  • 结束策略感觉是一大坑点,就是因为可能有换乘的请求还在其他电梯里跑,一定要通过Controller读取其他电梯的状态进行相应的处理(wait还是结束)。

(四)Bug分析

本次作业在强测中爆了一堆CTLE,开始的时候我觉得是我结束策略的问题,改了几种结束策略也没有什么效果,后来发现是换乘策略的两个Bug导致有些请求进入了Dispatcher的请求队列,但电梯一直无法处理这个请求只能不断轮询下述代码的第二行。

while (true) {
    if (dispatcher.requestIsEmpty() && passengers.isEmpty()) {
        if (controller.isInputEnd()) {
            if (controller.otherElevatorNoRequest()) {
                controller.notice();
                break;
            } else {
                controller.waitForInput();
            }
        } else {
            controller.waitForInput();
        }
    }
    //...处理请求
}

这两个Bug一是换乘请求换乘的请求没有预设needToChange这个变量为false,这就有可能导致该请求被下发到可以直达的电梯时还是needToChange的状态,以至于到了目的地还要再被Controller分配。

out(passenger);
if (passenger.isNeedToChange()) {
    passenger.setNeedToChange(false);	//没加这句就会导致该请求一直被下发
    passenger.setCurrentFrom(currentFloor);
    passenger.setCurrentTo(passenger.getDestination());
    controller.addRequest(passenger);	//controller分配请求到对应电梯
}

第二个Bug是换乘算法没有考虑到fromFloor和changeFloor一样的问题,就比如passenger from 18 to 14的请求,这时候我们的算法考虑的3号电梯(到达楼层1-3, 18-20)负载较小,该乘客被分配到了这个电梯,而三号电梯只能到18层,这个乘客就被卡在18层动不了了,电梯无法处理这个请求,只能不断轮询。


发现别人程序Bug所采用的策略

  • 把评论区里c语言+管道辅助定时输入的方法改进了一下简单的搭了个评测机,可以把别人的程序打成jar包后跑txt里边的数据,数据主要是手工构造的,比如同一时间很多个人同时从一楼出发,以及把多个加入电梯的请求放到所有请求的最后等,但是好像没有什么用,大家的程序跑得都挺好的o.o。

心得体会

线程安全

  • 在这三次作业中笔者没有遇到什么线程安全的问题,究其原因还是一直沿用的生产者-消费者模式,生产者-消费者这两个线程的交互集中在托盘中的请求队列来完成,只要给涉及请求队列的操作上锁就不会产生线程安全的问题,包括多个消费者可能存在交互的情况亦是如此,开始设计时尽量明确一个调度器类作为托盘处理线程可能的交互就能做到有备无患。

层次化设计

  • 层次化设计就是说我们要明确我们设计的每一个类都要做到各司其职,而且只干他该干的事,比如:我的设计需要几个线程?我需要给我的每个电梯配一个dispatcher吗?有没有必要设一个总的Controller?线程的交互发生在哪些地方?线程的交互能不能集中到一个类,是Controller还是dispatcher?明确了这些问题后总体的设计架构无疑也就跃然纸上了。
posted @ 2021-04-26 21:08  ihgc  阅读(96)  评论(0)    收藏  举报