BUAA_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类。
心得体会
-
通过与第一单元对比,我明显感觉到一个好框架的重要性,第一单元的重构经历让我难以忘怀,也就更加重视前期架构.避免重构.
-
这次作业使我理解到多线程的复杂,以及线程安全的重要性。
浙公网安备 33010602011771号