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,Controller) 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,Controller) 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,Controller) 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 复现如下:

  1. look() 检查 controller 状态,发现没有人需要接送,所以就等待,发出 WAIT 指令,放开 controller 的锁
  2. excute() 接到 WAIT 指令,但是还没有执行
  3. controller 接到 stop(),自杀
  4. 此时电梯应该自杀,因为 controller 已经死了,而且目前也没有人需要接送。然而,刚刚的 WAIT 还没执行
  5. excute() 执行指令,WAIT
  6. 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 的形式,或者说增大高层人员的比例,这样就诞生了新的优化策略:电梯没事的时候停留在顶层。

posted @ 2021-04-22 19:29  roife  阅读(949)  评论(2编辑  收藏  举报