BUAA_OO_Summary_StageTwo

一、作业设计分析

第五次作业

思路:

作业要求:

  • 本次作业仅要求一个电梯,基本没有任何限制

类设计(除去主类):

  • Dispatcher类:继承自Thread,本次作业调度器基本是个空壳子,直接与输入是在一起的
  • Elevator类:继承自Thread,模拟电梯运作
  • Person类:输入的请求转成Person类存放

线程设计:

  • 我选择使用了两个线程,输入线程和电梯线程

调度策略:

  • 在看了个各种电梯调度策略之后,发现好像并没有最完美的调度策略,最后选择采取了与ALS较为相近的可捎带策略,稍微做了一丁点的改动

线程安全性:

  • Dispathcer类设计为线程安全类
  • 考虑在对请求队列遍历过程中可能会有新请求加入,且除此之外不会做其余改动,不会出现其他安全问题,因而在遍历请求队列时未加锁

一些具体实现:

  • 调度器与电梯共享同一个请求池(请求池本次放在了Dispatcher中,设置了相关的static方法,使得电梯可以直接访问)
  • 电梯在每一层主动检查请求池,电梯调用checkIn()checkOut()方法,检查是否有人要进或者出

度量分析

UML图

各个类的复杂度分析

  • 分别有OCavg(类的方法平均循环复杂度)和WMC(总循环复杂度)两个指标,可见所有指标都没有飙红,复杂度尚可

总长400行左右

第六次作业

思路:

作业要求:

  • 本次作业要求按照输入的整数,开启3-5台电梯,每台电梯限乘7人

类设计(除去主类):

  • Person类:仍然是存放请求
  • Pool类:总的请求池,所有的方法和属性均为static,对所有类可见,所有类共享
  • Input类:继承自Thread,输入线程
  • ElevatorStatus类:存储电梯状态,电梯与调度器共享
  • RequestPool类:每个电梯独有的请求池,各电梯间不互通,分别与调度器共享
  • Elevator类:继承自Thread,模拟电梯运作
  • Dispatcher类:继承自Thread,调度器类,负责将总的请求池Pool中的请求分配给每个电梯的RequestPool

线程设计:

  • 本次作业输入线程和调度器线程是两个独立的线程,除此之外每个电梯一个线程

调度策略:

  • 电梯的调度策略:基本没有修改,只是将checkOut()方法中的请求池换成自己独有的那个

  • 调度器的调度策略:

    优先将请求分入可以捎带的电梯的请求池中,前提是此电梯请求池的人数加上电梯内的人数小于7

    无捎带的情况下,则优先寻找空电梯

    都不满足的情况下则调用PoolwaitForChange()方法等待,防止轮询,当Pool有新请求加入和任意一个电梯的目标楼层被改变时会唤醒Dispatcher,唤醒后遍历Pool中请求

线程安全性:

  • ElevatorStatus类、Pool类、RequestPool类均为线程安全类

线程思路分析

度量分析

UML图

各个类的复杂度分析

  • 电梯和输入类个人认为可修改性不大,调度器的方法在每次电梯状态改变时都会重新调用,导致循环复杂度偏高,还有修改空间

总长650行左右

第七次作业

思路:

作业要求:

  • 本次作业要求先开启3台电梯分别为A类、B类、C类,并且可根据输入新增对应类型的电梯
  • 不同类型的电梯有不同的可停靠楼层,移动速度和最大载客量

类设计(除去主类,比第二次作业新增):

  • ElevatorStatusFactory类:在每个ElevatorStatus中存储了电梯的各种信息,为了防止Input的run方法过于冗长,建立此类,根据新加入的电梯ID和类型创建

线程设计:

  • 本次作业仍是输入线程和调度器线程是两个独立的线程,除此之外每个电梯一个线程

调度策略:

  • 电梯的调度策略:基本没有修改,只是到了每层之后检查是否可以停靠,如果可以停靠才调用inAndOut()

  • 调度器的调度策略:

    先检查是否需要换乘,如果需要换乘对请求进行修改

    其余部分基本与上次作业一样,只需要检查是否可以乘坐该电梯

线程思路分析

度量分析

时序图

UML图

各个类的复杂度分析

  • 本次复杂度基本和第二次没有区别

总长800行左右

二、 可扩展性分析

  • 对最后一次作业进行可扩展性分析

基于S.O.L.I.D.原则:

SRP原则(单一责任原则):

  • 每个类各司其职,电梯类只负责自己电梯的调度,调度器负责将请求分配给各个电梯

OCP原则(开放封闭原则):

  • 本单元未进行重构,方法间也没有混杂在一起,基本满足OCP原则

LSP原则(里氏替换原则):

  • 在本单元作业中未用到继承

ISP原则(接口分离原则):

  • 本单元作业未使用到接口

DIP原则(依赖倒置原则):

  • 这一部分不能很好的把握,在编程过程中主要还是对具体类进行编写

三、Test与Bug

Bug

  • 在最后一次作业的互测中出现了一个bug,当最后一个人下了电梯准备进行换乘时,电梯停止运行了

  • 但是自己本地跑了上百遍也没能复现出来,无奈之下开始肉眼检查安全问题,最后发现一个很小几率会出现的bug

public synchronized void getOut(Person person) {//电梯的getOut方法
    SafeOutput.output("OUT-" + person.getId() + "-" +
                      status.getFloor() + "-" + status.getElevatorId());
    if (person.getNeedTrans() && !person.getHasTrans()) {//需要换乘的情况
        person.transfer();
        Pool.addRequest(person);
        status.out(person);
    } else {//不需要换乘的情况
        status.out(person);
        Pool.alert();
    }
}
public Boolean checkOver() {//调取器的checkover方法
    Boolean over = Pool.getOver() && Pool.isEmpty();
    for (int i = 0; i < status.size(); i++) {
        over = over && request.get(i).isEmpty() && status.get(i).isEmpty();
    }
    return over;
}
  • 由于checkOver()方法的非原子性,倘若只有一台电梯剩最后一个需要换乘的乘客,此时如果checkOver()方法先运行Pool.isEmpty()方法,之后运行了电梯getOut()方法,然后调度器再运行for循环的话,就会误判断此时请求队列和电梯均没有人,而判断应该结束。
  • 针对此bug,由于很难通过加锁的方式将该操作原子化,最终我选择通过更改checkOver()的执行顺序,将Pool.isEmpty()放在最后执行,利用逻辑将其避免

Test

由于线程安全问题不一定能稳定复现,我的主要测试是测试WA顺带测试线程安全的问题:

  1. 通过大量的自动生成数据集自动化定点投放输入测试+判定结果正确性

  2. 当发生错误时分析代码逻辑,定位错误原因,再测试出错的一批数据集

但是自己的线程安全问题没能在课下测出来TAT

四、心得体会

  • 跟单线程相比,多线程更考验我们的设计以及我们程序设计的严谨性和周密性。

  • 但是也不可为了安全滥用锁,一是锁太多很繁重,对锁的滥用也很容易产生一些死锁等安全问题。

  • 多线程因为不可复现、不可调试的特性,让大家的debug之旅更加痛苦了,尽管我在编程的过程中已经万分谨慎,但是线程安全问题他还是来了。

  • 多线程为了节省CPU的时间一定要用好wait和notify。

原文地址:https://www.cnblogs.com/Kidleyh/p/12698657.html