BUAA_OO_第二单元作业总结

2021北航OO第二单元(多线程_电梯调度)总结

本单元作业为多线程电梯调度.

共三次作业,迭代式任务.

第一次作业为是模拟单部多线程电梯的运行。电梯可达1-20楼层

第二次作业业要求模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。电梯可达1-20层.

第三次作业要求模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,以及最重要的——可停靠楼层的不同。

 

通过第一单元的锤炼, 我明白程序可扩展性的重要性,因此,在做第一次作业时,我就有有意识的考虑可扩展性,最终来看也很有效果,整个单元三次作业我并没有重构,每次作业都是在前一次的基础上扩充.因此,以下的分析将直接在第三次作业的基础上进行。

 

同步块设置和锁的选择

Main方法主要是开线程,按下不表。

多线程主要是考虑线程安全。我在Main中定义了LinkedBlockingQueue类,这就是Producer类与Elevator类的数据传递方式。因此,这也是锁的出现位置。因为如果当一方在操作时不锁住queue,会出现难以复现的线程安全问题。

锁的操作:

  • 在Producer类中,当有输入新的请求时,通过addRequestToQueue方法,先锁住queue,防止此时有电梯类读取,然后将请求加入queue中,最后notifyAll即可。

public static void addRequestToQueue(Object request, LinkedBlockingQueue<Object> queue) {
       synchronized (queue) {
           //放入queue中
           queue.notifyAll();
      }
  }

 

  • 在Elevator类中,通过getFronQueue方法将queue的请求取出加入到电梯自己的调度器中。先锁住queue,防止此时Producer往queue中添加请求,然后,当queue不为空,取出全部请求,当执行尚未结束时,通过wait释放锁。

private void getFromQueue() {
synchronized (queue) {
       while (!queue.isEmpty()) {
           //全部取出放到电梯的调度器中
      }
       if (/*尚未结束*/) {
      queue.wait();
      }
}
}

 

调度器设计

我的架构中,分配器设计与Producer类在一起,即读取请求就考虑了分配策略。将该请求分配给指定的电梯调度器中。

对于电梯的调度器,根据三种不同模式(Morning, Night, Random模式)有不同的策略。

实现:通过电梯内部的候乘Map(分为upMap和downMap,以出发层为key值,value值为ArrayList类,存放当前所有从该层出发的请求),电梯根据当前运行方向来决定从upMap或者downMap中取请求(相当于电梯门口排队的人进电梯)。电梯内部以treeMap(以到达楼层为key值,这样,可以一次性将所有当前楼层为目的楼层的请求全部找到)存放电梯内的请求。

这里是不需要考虑线程安全的,因为只有一个线程,执行时会按照代码逻辑的先后顺序。

 

 

扩展性分析

由于三次作业并没有重构,因此,笔者只选取第三次作业代码来分析.

第三次作业共三个类

第一个是主类,主要用于创建线程.

 

 

第二个是Producer类,用于读取请求以及分配.(请举略命名)

 

 

第三个是Elevator类,通过指令来设置电梯的属性,以及运行状态.内部数据结构较多,包括电梯内部的调度器以及电梯乘员表.

 

 

可以看到,由于我的类比较少,因此,每个类内部逻辑稍显复杂,.这也是我需要提高的一点.

 

1.MainClass类分析

 

 

我的MainClass类主要用于启动线程,因此方法简单.

 

2.Producer类分析

 

 

可以看到,由于我将producer和dispatcher合并写, 导致不够耦合,代码逻辑稍显紊乱.

更好的优化措施是将该类拆成2个,一个是producer类来读入请求,另一个是dispatcher类来根据一定的策略对请求进行分配.

 

3.Elevator类分析

 

 

通过以上度量可以看到,该类内部逻辑紊乱,代码复杂,主要体现在各种策略的处理上.Morning,Night,Random三个策略我都放在Elevator类中,使得代码规模过大,达到450行,可读性不高,同时,为了处理某种策略,又抽象出各种方法,如isEmpty()等,使得整个代码逻辑紊乱.

更好的优化是使用策略类,电梯类只管用策略类来执行请求,而不用管策略细节.这也是我欠缺的一种能力------抽象能力.

因此,可扩展性并不佳。当需求增加时,存在重构风险。

 

 

 

 

通过以上UML协作图可以看到,我的设计不够面向对象,一个类包含了太多功能,这样,当遇到新的需求时,难以扩展。希望下一单元作业我能够解决这个问题。

 

 

逻辑分析

 

 

总体逻辑比较简单,根据请求特点(奇数层到奇数层/低三层与高三层之间/其余)分配给指定电梯。

 

 

 

Bug分析

第一次作业并没有bug,由于采用了look算法,性能还不错.

第二次作业存在一个bug,我的程序由于在第一次运载请求时并没有尽可能最多的进入电梯,因此,导致运行超时了(超时十多秒).因此,我优化了第一次有请求时的策略,等2秒或者达到最大容量之后再运送请求,顺利解决了这个bug.

另一方面,由于我的synchronize使用错误,起初是对run方法整个加锁,导致Producer类无法往Elevator类分配请求,必须等run方法将候乘列表全部运送完之后才能往候乘列表中添加请求,但事实上,候称列表只需要在往电梯中输入(IN)时才需要加锁,这也极大降低了性能.最终, 我在run方法中循环开始时就加锁,一次性全部取出,释放锁后,再进行电梯运送请求.性能大幅提高.

第三次作业也没有bug.(并没有采用换乘策略,性能分竟然还不错。)

 

hack别人

由于不会向程序定时投放请求,因此,我选择阅读分析别人代码.

第一次作业我发现一个人代码中电梯存在对请求1-FROM-1-TO-20无法处理的情况,因此我成功hack他人一次.

第二次作业由于对性能有要求,我选择构造一些边界数据,成功hack出别人两个bug.

第三次作业可以考虑换乘,策略较多,主要是从超时的角度去hack,但并没有成功hack到.

 

重构经历

由于我没有重构,因此,没有重构经历.(逃)

不过,在写代码的过程中,由于没有很好的架构思路就开始写,因此,导致为了方便的实现某种功能,我需要把我的数据结构做变换。

  • 把Arraylist换成Map,用出发楼层作为key值,这样,无需遍历Arraylist就可以获得从某一楼层出发的全部请求。

  • 把数据同步用线程安全类(LinkedBlockQueue)来实现,这样,整个逻辑就自然而然而无需维护过多的锁。

  • 添加输入结束标志,当输入结束时,将String = ”inputEnd“传给每个电梯队列。因此,我将队列中的元素有Request换成了Object类。

 

 

心得体会

  • 通过与第一单元对比,我明显感觉到一个好框架的重要性,第一单元的重构经历让我难以忘怀,也就更加重视前期架构.避免重构.

  • 这次作业使我理解到多线程的复杂,以及线程安全的重要性。

  • 感觉到自己面向对象设计与构造能力有所提高。

posted @ 2021-04-22 00:44  刘bh  阅读(117)  评论(1)    收藏  举报