BUAA OO 第二次作业总结
BUAA OO 第二次作业总结
架构与线程安全策略分析
第五单元
代码架构
其中,各个类的含义如下:
|- Main:主类
|- Controller:调度器(队列)
|- InputHandler:读入线程
|- Elevator:电梯线程
|- Action(enum):指令集合
我的代码中总共有两个线程:电梯线程 Elevator
和输入线程 InputHandler
。
而调度器 controller
并不是线程,是一个同步对象。这样可以避免两个线程进行直接交互,避免了多线程交互时数据同步的问题。
同步块与锁分析
本次作业中,我的 controller
类是同步对象,所以对 controller
资源的访问都是带锁的(用 synchronized
实现)。
一方面,InputHandler
的任务是获取输入,并且发给 controller
,此时使用 synchronized
标记的 addRequest
。
另一方面,Elevator
的任务是从 controller
中获取 request 并且决定电梯下一步怎么走。首先调用 look()
函数使用 LOOK 算法分析下一步的走法,然后返回指令。接着调用 excute()
方法执行指令。
电梯对同步块的访问基本都是修改型的,例如“添加 request”,“获取 request”,如果不加锁,可能就会造成资源的共享导致错误(例如一个人同时进了两个电梯)。
唯一的一个例外是 checkAlive()
方法,其作用是检查 controller
有没有死掉,然后会考虑是否要进入 wait
状态。如果不加锁,可能导致刚刚死掉,电梯就进入等待了,使得电梯无法自杀,最后导致 RTLE。
调度器分析
调度器一共 5 个方法:
checkAlive
:判断输入是否结束stop
:由InputHandler
调用,负责通知结束输入addRequest
:由InputHandler
发出,负责添加乘客getFloorWaiting()
和getWaiting
:由Elevator
发出,获取等待队列
这五个方法都用 synchronized
标记,所以 controller
始终只会被一个线程独占。也就是说,所有和 controller
的交互都是原子性的,所以不会发生线程安全问题。
性能分策略
性能上,我使用了 LOOK 算法,其思想如下:
- 如果电梯没人
- 如果没请求,则等待
- 如果有请求,沿原方向前进(原来是等待则沿另一个方向前进)
- 如果电梯有人
- 判断当前是否需要开门
- 如果不用开门,则沿原方向前进
我使用的另一个优化是,电梯如果在等待状态,则默认开门接人。在向上向下或者结束时,关闭电梯门。关闭电梯门的时候检查距离上次开门是否已经过了 openTime + closeTime
,如果过了这个时间直接输出关门指令;否则等待一点时间再关门。
第六单元
代码架构
|- Main:主类
|- Controller:调度器(队列)
|- InputHandler:读入线程
|- Elevator:电梯线程
|- Action(enum):指令集合
代码几乎没怎么变,因为这个单元只多了一个增加电梯的要求,所以我只在 InputHandler
的输入里面处理了一下增加电梯并启动线程,其他代码均未变。
同步块与锁分析
这一单元和上一单元几乎没什么差别,所以我也几乎没有对同步块进行修改。
但是本单元我被互测找出了一个 BUG,分析原因认为是我在第一单元考虑同步关系时,采用了类似于 CPU 的模式:即 CU 发出指令,然后电梯执行。但是在发出指令和执行间的间隔有可能会使得电梯状态发生变化,最终导致指令错误。
解决的一个方案是将指令发出和指令执行发在同一个同步块内,但是这样会发生阻塞。另一个方案是在等待之前都重新检查状态。
经过权衡,我使用了方案二,仅仅添加了一行 if
语句解决了问题。
调度器分析
和第五单元一样,没有更新。
性能分策略
我在本单元采用的是自由竞争的策略,也就是如果有人来,那么就所有电梯一起出发,谁先到就由谁接。
这样做的好处是调度简单,不需要额外的逻辑,避免了线程安全的问题。坏处是和现实中的电梯逻辑不同。
但是经过测试,这样的电梯效果确实还不错,而且减少了代码量。
第七单元
代码架构
|- Main:主类
|- Controller:调度器(队列)
|- InputHandler:读入线程
|- Elevator:电梯线程
|- ElevatorFactory:电梯工厂
|- Action(enum):指令集合
为了应对多种电梯的需求,我是用工厂方法用了一个 ElevatorFactory
类来获取电梯。
同步块与锁分析
本次作业主要更换了调度策略,在同步块和锁上的方案与第一次作业一致。
调度器分析
在本次作业中,为了支持换乘,我为每种电梯单独分配了等待队列,防止多种电梯干扰。
调度器新增了一个方法:reAddPersonRequest
:放回请求,用于换乘
为了支持换乘,我使用的策略是读入请求的时候按照换乘策略进行拆分,将前一半作为 key,后一半作为 value 放入一个 hashmap 中。当有人从电梯门出来的时候就检查 hashmap 中是否有换乘的需求,如果有需求则在请求队列中添加换乘请求。
性能分策略
此次作业增加了换乘的选择,我使用的策略如下:
- 如果想去 16+ 层,且人在 4 层,则去 3 层换乘到 18+ 层,再换乘过去
- 如果想去 3- 层,且人在 16 层,则去 18 层换乘
- 否则,尽量去奇数层换乘
扩展性分析
UML 类图
第五次作业
第六次作业
第七次作业
协作图
分析
我认为我的第三次作业可扩展性较强。
从功能上说,我认为未来添加的请求很可能在电梯种类和楼层种类上做文章,并且假设不会对题目含义做大的变动,所以我在这个基础上尽量做到性能最优。
如果要添加新种类的电梯,那么只需要在 ElevatorFactory 中修改即可。然后再根据需求可以修改换乘策略,使得新的电梯可以完美进入工作。
另一方面,如果要增加楼层,也只需要修改常数,不需要对代码架构进行大的变动。
代码复杂度分析
第五次作业
度量分析
方法 | OCavg | OCmax | WMC |
---|---|---|---|
Elevator | 2.8333333333333335 | 8.0 | 34.0 |
InputHandler | 2.0 | 3.0 | 4.0 |
Controller | 1.1666666666666667 | 2.0 | 7.0 |
Main | 1.0 | 1.0 | 1.0 |
Action | 0.0 | ||
Total | 46.0 | ||
Average | 2.1904761904761907 | 3.5 | 9.2 |
从中可以看出我的逻辑复杂度主要集中在电梯类上,这是因为我将逻辑都放在了 Elevator
这个类中。
可以考虑将电梯的“状态”和“逻辑”分成两个类。
方法圈复杂度分析
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Controller.addRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.getFloorWaiting(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.getWaiting() | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.stop() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.Elevator(int,int,int,int,int,TreeSet |
0.0 | 1.0 | 1.0 | 1.0 |
Elevator.isEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.isFull() | 0.0 | 1.0 | 1.0 | 1.0 |
InputHandler.InputHandler(ElevatorInput,Controller) | 0.0 | 1.0 | 1.0 | 1.0 |
Main.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.Controller(String,int) | 1.0 | 1.0 | 2.0 | 2.0 |
Controller.checkAlive() | 1.0 | 1.0 | 1.0 | 2.0 |
Elevator.checkAlive() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.open() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.close() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.down() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator.putOffPassengers() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.up() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator.getOnPassengers() | 5.0 | 3.0 | 3.0 | 4.0 |
InputHandler.run() | 5.0 | 3.0 | 4.0 | 4.0 |
Elevator.run() | 7.0 | 1.0 | 4.0 | 8.0 |
Elevator.look() | 16.0 | 6.0 | 6.0 | 12.0 |
Total | 47.0 | 30.0 | 45.0 | 57.0 |
Average | 2.238095238095238 | 1.4285714285714286 | 2.142857142857143 | 2.7142857142857144 |
如图,主要的复杂度集中在 look()
算法中,这是因为我的电梯运行逻辑集中在 look()
算法,所以逻辑比较复杂。
第六次作业
度量分析
方法 | OCavg | OCmax | WMC |
---|---|---|---|
InputHandler | 3.0 | 5.0 | 6.0 |
Elevator | 2.357142857142857 | 8.0 | 33.0 |
Controller | 1.1666666666666667 | 2.0 | 7.0 |
Main | 1.0 | 1.0 | 1.0 |
Action | 0.0 | ||
Total | 47.0 | ||
Average | 2.0434782608695654 | 4.0 | 9.4 |
和上次差不多,因为几乎没有修改代码。
方法圈复杂度分析
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Controller.addRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.getFloorWaiting(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.getWaiting() | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.stop() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.Elevator(String,int,int,int,int,int,TreeSet |
0.0 | 1.0 | 1.0 | 1.0 |
Elevator.isEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.isFull() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.putOffPassengers() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.sameDir(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
InputHandler.InputHandler(ElevatorInput,Controller,ArrayList |
0.0 | 1.0 | 1.0 | 1.0 |
Main.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.Controller(String,int) | 1.0 | 1.0 | 2.0 | 2.0 |
Controller.checkAlive() | 1.0 | 1.0 | 1.0 | 2.0 |
Elevator.checkAlive() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.getOnPassengers() | 1.0 | 2.0 | 1.0 | 2.0 |
Elevator.close() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.down() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator.run() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator.up() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator.open() | 4.0 | 1.0 | 4.0 | 4.0 |
Elevator.execute(Action) | 3.0 | 1.0 | 2.0 | 5.0 |
InputHandler.run() | 6.0 | 3.0 | 6.0 | 6.0 |
Elevator.look() | 18.0 | 6.0 | 9.0 | 14.0 |
Total | 44.0 | 31.0 | 50.0 | 60.0 |
Average | 1.9130434782608696 | 1.3478260869565217 | 2.1739130434782608 | 2.608695652173913 |
同上,和上次差不多,因为几乎没有修改代码。主要问题还是集中在 look()
方法中。
第七次作业
度量分析
方法 | OCavg | OCmax | WMC |
---|---|---|---|
ElevatorFactory | 5.0 | 5.0 | 10.0 |
Controller | 3.0 | 13.0 | 30.0 |
InputHandler | 3.0 | 5.0 | 6.0 |
Elevator | 2.4285714285714284 | 8.0 | 34.0 |
Debug | 1.5 | 2.0 | 3.0 |
Main | 1.0 | 1.0 | 1.0 |
Action | 0.0 | ||
Total | 84.0 | ||
Average | 2.7096774193548385 | 5.666666666666667 | 12.0 |
类复杂度上几乎没有变化,其中工厂方法之所以复杂度较高,是因为选择路径变多了。
方法圈复杂度分析
方法 | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Controller.addElevator(ElevatorRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.addTransferRequest(PersonRequest,PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.getFloorWaiting(String,int) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.stop() | 0.0 | 1.0 | 1.0 | 1.0 |
Debug.addElevator(Elevator) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.Elevator(String,String,int,int,int,int,int,TreeSet |
0.0 | 1.0 | 1.0 | 1.0 |
Elevator.isEmpty() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.isFull() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.putOffPassengers() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.sameDir(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
InputHandler.InputHandler(ElevatorInput,Controller) | 0.0 | 1.0 | 1.0 | 1.0 |
Main.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Controller.Controller(int) | 1.0 | 1.0 | 2.0 | 2.0 |
Controller.checkAlive(String) | 1.0 | 1.0 | 1.0 | 2.0 |
Controller.reAddPersonRequest(PersonRequest) | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.checkAlive() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.getOnPassengers() | 1.0 | 2.0 | 1.0 | 2.0 |
Controller.addRequestHelper(PersonRequest) | 3.0 | 1.0 | 3.0 | 3.0 |
Debug.run() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.close() | 3.0 | 1.0 | 3.0 | 3.0 |
Elevator.down() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator.run() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator.up() | 2.0 | 1.0 | 3.0 | 3.0 |
Controller.getWaiting(String) | 1.0 | 4.0 | 1.0 | 4.0 |
Elevator.open() | 4.0 | 1.0 | 4.0 | 4.0 |
ElevatorFactory.createElevator(String,String,Controller) | 1.0 | 4.0 | 1.0 | 4.0 |
ElevatorFactory.checkReachable(String,PersonRequest) | 2.0 | 1.0 | 2.0 | 5.0 |
Elevator.execute(Action) | 6.0 | 1.0 | 3.0 | 6.0 |
InputHandler.run() | 6.0 | 3.0 | 6.0 | 6.0 |
Elevator.look() | 17.0 | 6.0 | 8.0 | 13.0 |
Controller.addRequest(PersonRequest) | 34.0 | 1.0 | 13.0 | 21.0 |
Total | 91.0 | 45.0 | 76.0 | 103.0 |
Average | 2.935483870967742 | 1.4516129032258065 | 2.4516129032258065 | 3.3225806451612905 |
由于新增了换乘的判断,导致一些方法复杂度超标,主要集中在换乘策略上。
如果要进一步优化方法圈复杂度,可以考虑将换乘策略单独封装成一个类,然后按照不同的条件调用不同的方法。
BUG 分析
三次作业被 hack 了一次,主要是同步的问题。
我的程序只有一个 controller
是作为共享资源的,并没有分出细粒度更小的共享对象,所以反而不会出现死锁的问题。
然而我还是遇到了同步问题,原因集中在我的架构设计上。我的处理方式是由 look()
函数根据当前的情况发出具体的指令(例如开门/上下楼/等待等),然后由 excute()
函数执行指令。然而,问题出现在 WAIT
这个指令上,它可能导致 RTLE。
经过我的多次尝试,BUG 复现如下:
look()
检查controller
状态,发现没有人需要接送,所以就等待,发出WAIT
指令,放开controller
的锁excute()
接到WAIT
指令,但是还没有执行controller
接到stop()
,自杀- 此时电梯应该自杀,因为
controller
已经死了,而且目前也没有人需要接送。然而,刚刚的WAIT
还没执行 excute()
执行指令,WAIT
- RTLE
其实这个 BUG 修复起来也很简单:只要在 controller.wait()
前面判断其是否存活即可:
synchronized (controller) {
if (controller.checkAlive()) {
controller.wait();
}
}
Hack 策略
随机数据 + 多线程爆破,多核跑测试效果更好。
然而并没有发现很多问题,而且一些数据交上去也 Hack 不掉它,所以只 Hack 了一个人(悲)。而且即使是我自己测试,也不一定能复现 BUG。
多线程 Hack 真的好麻烦哦。
对课程的想法
个人感觉这次作业加的”Morning“/”Night“/”Random“模式可以稍微再改进一下。因为按照现在的方案,实际上 Night 如果都用 Random 做,其效果是更好的:
- 对于 Night,主要的性能问题可能在于由于同步的原因,输入不是同时到达。然而无论如何,由于人都是在二层及以上的,所以首先电梯下一时刻必须要去二层。此时尽管存在同步问题,但是人肯定已经全部到齐了,所以反而已经达到了原先的目标效果。由此可见,所以不对 Night 进行优化反而是更好的策略!
所以我建议 Night 改成类似于 Morning 的形式,或者说增大高层人员的比例,这样就诞生了新的优化策略:电梯没事的时候停留在顶层。