OO第二单元总结 电梯调度问题

OO第二单元总结 电梯调度问题

多线程老折磨王了

一、设计策略分析

主要的类:

  • 主要应用生产者消费者模式进行设计,第三次作业利用JAVA多态特性。
  • Main主线程中运行课程组下发的input,作为生产者。同时创建调度器,创建和运行电梯线程。
  • RequestList类有两个功能。首先它内部有一个List,来存放生产者产生的请求。同时它也是一个调度器,根据电梯的信息与状态提供相应请求给电梯。由于它是共享的,要实现线程安全。(具体方式:每个方法无脑加锁)
  • Elevator线程作为消费者,实现每个请求。初始化电梯各个属性,规定电梯的运行捎带策略。

主要策略:

  • 电梯的捎带策略采用比较简单的ALS捎带方法,在此基础上作改进,允许被捎带的请求成为主请求。
  • 调度器提供getFirst()getCurrentFloor()方法。前者在电梯为空时获取主请求,后者在电梯到达每一层时根据电梯状态进行捎带。
  • getFirst()无法取得任何请求时,要进入睡眠。而getCurrentFloor()无需如此。
  • turnoff()(提示输入已结束)、put()(向List中加入请求)、setReady()(使换乘请求能够被执行)三个方法都会使用Notifyall()唤醒睡眠的电梯进程。
  • 当存在多部电梯时,调度器不会主动将请求分配给对应电梯。而是电梯每到一层时与调度器进行交互,取得可以捎带的请求。请求的执行具有随机性
  • 在第三次迭代中,创建DirectPersonRequest类,继承自PersonRequest类,其中多加了一个列表,存放着可用于执行这条指令的电梯类型。
  • 将要换乘的指令拆分成两条请求,前一条请求执行完后才能进行后一条请求的执行。创建DividePersonRequest类,继承自DirectPersonRequest,多加了一个属性ready来判断其是否能被执行。

二、架构设计可扩展性

  • SRP(The Single Responsibility Principle)单一责任原则:每个类或方法都只有一个明确的职责。

    ​ 这系列作业架构比较清晰,Main线程负责输入、初始化和结束,RequestList类负责调度,Elevator类负责电梯运行,类中的方法并不是很复杂。改进方面,可以将input单独设置成一个线程运行,将RequestList类分成请求队列和调度器两部分。

  • OCP(The Open Closed Principle)开放封闭原则:无需修改已有实现,而是通过扩展来增加新功能。

    ​ 在系列的迭代,拓展的新功能:电梯初始属性设置、电梯虚拟楼层和现实楼层映射、请求的分配、换乘请求的分配,这些功能的实现都没有大量修改已有代码。

  • LSP(The Liskov Substitution Principle)里氏替换原则:任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误。

    DirectPersonRequestDividePersonRequestPersonRequest是继承关系,它们都拥有获得请求的from、to以及id的功能。如果未来出现了需要换乘两次的请求(魔鬼),也可以同样处理。

  • ISP(The Interface Seqreqation Principle)接口分离原则:通过接口来建立行为抽象层次具有更好的灵活性。

    ​ 未使用接口。

  • DIP(The Dependency Inversion Principle)依赖倒置原则:代码依赖于抽象,不要依赖于具体。

    ​ 各个类之间并没有很强的具体依赖关系,都是通过方法进行通信。

三、基于度量的程序结构分析

1.第一次作业

UML类图

OO第二单元总结 电梯调度问题_第1张图片

代码行数

代码复杂度

OO第二单元总结 电梯调度问题_第2张图片

2.第二次作业

UML类图

同第一次作业

代码行数

代码复杂度

OO第二单元总结 电梯调度问题_第3张图片

3.第三次作业

UML类图

OO第二单元总结 电梯调度问题_第4张图片
OO第二单元总结 电梯调度问题_第5张图片

代码行数

代码复杂度

OO第二单元总结 电梯调度问题_第6张图片

4.协作图

OO第二单元总结 电梯调度问题_第7张图片

5.分析

可以看到有几个方法明显复杂度较高。

  • assginElevator()方法是判断请求是否需要换乘,如果不需要换乘,则找到合适的电梯,需要换乘,则拆分成两个请求。我把这部分逻辑全部杂糅在了一个方法中,导致复杂度较高。
  • 由于我只设置了一个列表,而把请求能搭载的电梯信息放在请求内部,所以get()方法需要循环遍历整个列表,判断其是否为直达请求,是否能被这部电梯搭载,循环判断多。
  • run()方法中引用了很多重复的方法,模块耦合度高。

四、程序BUG分析

1.第一次作业

​ Elevator调度算法设计有问题。在电梯列表为空,运行去获取主请求时,到达每一层时并没有进行捎带,这造成了很大的时间和效率损失,导致程序运行时间过长。

2.第二次作业

​ Elevator电梯人数有问题。在每一层上下楼时,我不加思考地采用了先上后下的方法,在这个过程中导致人数超过上限。

3.第三次作业

​ 开始遇到了某线程无法结束的问题,主要原因出在RequestList类的getFirst()这个方法中,由于自己将所有请求放在一个队列中,判断条件逻辑相对复杂。强测互测无问题。

五、分析BUG策略

1.自动测试

  • 随机生成数据,并通过python的subprocess模块传入目标文件中实现定时投放。检查输出是否符合要求。
  • 同时根据数据也可以复现问题。

2.边缘测试

  • 针对电梯超载问题,在初始同一时刻就投入大量可捎带的请求,判断电梯人数是否符合规范。
  • 特别检查换乘人员中转站的上下时间和顺序。

3.线程安全测试

  • 主要体现在死锁、线程无法结束等问题上。
  • 由于把共享对象的方法加了锁,主要考虑wait()和notify()的行为和时机是否正确。
  • 通过输出调试信息发现问题。
  • 通过静态代码分析解决问题。

4.与第一单元差异

  • 生成测试数据简单了不少。
  • 测试的输入过程需要做到定点投放:测试也是一个并发的问题。
  • 判断输出数据的逻辑比较复杂,从评测的角度考虑的因素比较多。
  • 更多关注线程安全。

六、心得体会

  • 前两次作业相对简单成绩却不理想:越简单却不能轻视。
  • 短短几行代码问题,就挂了多个点:仔细考虑每个细节。
  • 生产者消费者模式的应用:正确的架构很重要,第一次有了迭代开发的感觉。
  • 性能分问题:不同的策略对性能分影响并没有那么大,还是主要考虑架构的正确和线程安全问题。同时这也是一个非常贴近现实的问题,就像数学建模一般,我们面对实际问题时要考虑的因素十分多,要从中有所取舍,就像存储系统一样,在时间与空间中有所协调折中。
  • JAVA的线程安全机制:本次实验只用到了synchronized给方法加锁。(甚至由于IDEA的智能输入功能还不知道这词怎么拼)其他锁机制如ReentrantLockReadWriteLock有更高的效率,但同时也对编程的要求更高,没有很好地实践与应用。
  • 线程安全问题:DEBUG难度大,花费时间多,还是要在程序设计初就搞清楚。
  • 多线程老折磨王了。

你可能感兴趣的:(OO第二单元总结 电梯调度问题)