BUAA_2022_OO_Unit2总结

2022_OO第二单元总结

一、架构分析

1.homework5 简单的生产者消费者模型

UML类图:
image

  hw5没有做过多的要求,五个座的电梯都是独立运行,比较简单。一开始我也是按照输入->调度器->电梯进行的架构,但是后来发现调度器实际上就是根据指令的座号分配到相应的队列中,没有起到调度的作用,而且浪费了一个线程,所以后来做了优化,把调度器并入了Input线程中,接收到输入信息后直接分发到对应的座(电梯)中。另外,我没有使用官方包里的构造方法,而是自己重写了Person类,又新增了PersonQueue类(借鉴了上机的架构),方便处理Person队列中的插入与删除的线程安全,每个电梯线程和Input线程都由一个PersonQueue对象作为托盘。

  这样做相当于每座只用了一个托盘,就完成了输入到电梯执行的信息传递,相对之前的架构更高效;但是这也导致了Input线程不仅要处理输入,还要兼顾分发,为hw7的复杂结构埋下了隐患(hw7迫不得已又增加了调度器)。

  电梯算法方面,三次作业我都使用了look算法,尽量不转向以提高效率。同学们很多都是用的这个算法,hw6和hw7的架构分析中不再赘述。

2.homework6 环形电梯和多电梯

UML类图:
image

  hw6中增加了横向环形电梯和每层/每座多电梯同时运行的要求,没有换乘要求(课程组的仁慈)。大的结构方面,我没有进行大的改动,线程还是只有Input和Elevator两类,另外添加了SafeOutPut类,方便安全输出,不用逐个加锁。

  对于横向电梯,我沿用了纵向电梯的的look算法。为了方便捎带,我在Person类中追加了两个状态 isUp 和 isRight,实例化Person对象时就保存了这个人向上/下、向左/右的请求,进行捎带时直接读取人的方向状态即可,省去了实时判断的步骤。

  对于同座/同层多电梯,我进行了调度器和自由竞争两方面的尝试。调度器的优点是分配透明,每个人分配到的电梯是可查询的,但缺点是写出一个效率高的动态调度器是十分麻烦的,要持续读取每个电梯的运行状态,首先电梯状态共享就是一个问题,容易出现线程安全问题,其次调度算法也无法保证最优;自由竞争的优点是方便好写,只要每座/每层的所有电梯共享同一个队列(PersonQueue),做好共享队列的线程安全即可,而且根据大量数据的检验,自由竞争的效率也很好,但缺点是有时候电梯会“陪跑”,人数少的时候浪费资源,一个人能进哪个电梯也是随缘的。

  经过深思熟虑(水平不行而且懒),我还是选择了自由竞争的策略,但写的过程中也发现,自由竞争并不是那么省事。比如刚写成的时候出了这么一个bug,如果多个电梯同时到达一层,即使这层只有一个人,每个电梯还是都会开门,浪费了停下的时间。这个bug我通过给人记录一个属性:要上的电梯ID 解决了。另外就是大家基本上都做过的优化:量子电梯,也就是停满0.4秒后可以闪现到相邻层/座中,这个我通过调用并记录系统时间解决了。

  至于动态新增电梯,算是比较简单的一部分,只要把新增的电梯共享到相应的队列中即可,不再赘述。

3.homework7 换乘和电梯定制

UML类图:
image

  hw7增加了电梯定制和换乘的要求,算是一个电梯系统的“完全体”,但只有横向电梯能定制停止座号,纵向电梯还是每层都能停,也算是课程组放了一些水(感谢)。

  大的结构方面,由于一开始的结构中Input功能过多,导致扩展能力不足,没法进行乘客路径的重分配,所以还是花了比较多的时间,增加了Distributor类,专门用于建立路径和分配到相应队列,也解放了Input,这次架构中的Input只需要初始化队列,处理新建乘客和新建电梯即可。另外把原来的Person类改成了PersonReq,新增的Person类只有一个ArrayList,按顺序保存PersonReq,也就是路径。

  电梯定制算是这次作业的开胃菜,只要把速度、容量和可停靠信息在新建电梯的时候输入进去即可,无需过多赘述。

  重头戏自然是换乘的调度策略,我的策略也是一步步复杂起来,最后停在了大概中等复杂的水平(还是太菜了)。一开始完全是纯静态分配,新建乘客对象的时候就确定了整个路径,不能充分利用后续加入的电梯,并且电梯的选择也有一定的问题,容易很多人堆在一个电梯里;后来改进了算法,每新增一个横向电梯,就把所有等待中乘客的路径重新规划一次,电梯的选择也倾向于平均分配,下面是详细解释:

  首先判断是否需要横向移动,如果不需要,则直接一段纵向路径即可;如果需要横向移动,则如下图:
image

  首先遍历所有的横向电梯,如果此电梯在起点座和终点座都能开门,则进行下一步判断:

  (1)如果该电梯楼层等于出发层或终点层,则将该层设为中转层;

  (2)如果该电梯楼层不等于出发层或终点层,如果|电梯层 - 出发层| + |电梯层 - 终点层| < minNum,则将该层设为中转层,并更新minNum;

  由于遍历一次后,选中的总是队列中最后一个满足条件的电梯,所以遍历完成后把该电梯调到队列首,达到平均分配的效果;

  这个分配策略胜在比较简单,但是没有考虑每层可用电梯数和电梯繁忙程度,而且所有横向路径都只有一段,不会出现A->B, B->C等情况,不能保证最短路径。

  附上hw7的协作图:
image

二、同步块设计

三次作业同步块设置相似,取最后一次详细介绍:

Input类中的同步块

synchronized (inputQueues) { //新建每层、每座的自由竞争队列
    for (i = 0; i < 5; i++) {
        PersonQueue input2ele = new PersonQueue(id1[i]);//座自由竞争队列
        inputQueues.add(input2ele);
        Elevator newEle = new Elevator(i + 1, (char) ('A' + i), 1, "building",
                8, 0.6, 0, input2ele, personList);
        elevators.add(newEle);
        newEle.start();
    }
    String[] id2 = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "10"};
    for (i = 0; i < 10; i++) {
        PersonQueue input2ele = new PersonQueue(id2[i]);//层自由竞争队列
        inputQueues.add(input2ele);
        if (i == 0) {
            Elevator newEle = new Elevator(6, 'A', 1, "floor",
                    8, 0.6, 31, input2ele, personList);
            elevators.add(newEle);
            floorEle.add(newEle);
            newEle.start();
        }
    }
}

synchronized (floorEle) { //每当加入横向电梯,把该电梯加入到所有横向电梯的队列中
    floorEle.add(newEle);
    alert();
}

Elevator中的同步块

synchronized (personOut) { //处理队列中请求,判断电梯运行方向和是否开关门,若没有请求则等待
    command = buildingSch();
    if (command == null) {
        try {
            //System.out.println("ele is wait: " + elevatorId);
            personOut.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

synchronized (personIn) { //电梯下人
    Iterator<Person> it = personIn.getQueue().iterator();
    while (it.hasNext()) {
        Person person = it.next();
        PersonReq tmp = person.getReq();
        if ((elevatorType.equals("building") && tmp.getToFloor() == elevatorFloor) ||
                (elevatorType.equals("floor") && tmp.getToBuilding() == elevatorSeat)) {
            SafeOutput.output("OUT-" + tmp.getPersonId() + "-" +
                    elevatorSeat + "-" + elevatorFloor + "-" + elevatorId);
            tmp.removeLock();
            //System.out.println(tmp.getPersonId() + " is unlocked!");
            it.remove();
            person.removeReq();
            person.changeFromF(elevatorFloor);
            person.changeFromB(elevatorSeat);
            personList.addPerson(person);
        }
    }
}

synchronized (personOut) { //电梯上人
    Iterator<Person> it = personOut.getQueue().iterator();
    while (it.hasNext() && personIn.getQueue().size() < capacity) {
        Person person = it.next();
        PersonReq tmp = person.getReq();
        if ((elevatorType.equals("building")
                && tmp.getFromFloor() == elevatorFloor
                && tmp.getIsUp() == nowUp) ||
                (elevatorType.equals("floor")
                        && tmp.getFromBuilding() == elevatorSeat)) {
            if (tmp.getLockNum() == elevatorId) {
                personIn.addPerson(person);
                SafeOutput.output("IN-" + tmp.getPersonId() +
                        "-" + elevatorSeat + "-" + elevatorFloor + "-" + elevatorId);
                it.remove();
            }
        }
    }
}

Distributor中的同步块

synchronized (personList) { //如果没有要分配的人,则wait
    if (personList.isEmpty()) {
        try {
            //System.out.println("dis is wait: personList is Empty!");
            personList.wait();
            //System.out.println("dis finish wait: personList is Empty!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

synchronized (floorEle) { //遍历全体横向电梯,找出中转层
    Elevator choosedEle = floorEle.get(0);
    Iterator<Elevator> it = floorEle.iterator();
    while (it.hasNext()) { //获取midfloor
        Elevator ele = it.next();
        eleStopSeat = ele.getStopSeat();
        eleFloor = ele.getElevatorFloor();
        if (((eleStopSeat >> (fromBuild - 'A')) & 1) == 1 &&
                ((eleStopSeat >> (toBuild - 'A')) & 1) == 1) {
            if (eleFloor == fromFloor || eleFloor == toFloor) {
                midFloor = eleFloor;
                choosedEle = ele;
                break;
            } else {
                double distance = Math.abs(eleFloor - fromFloor) +
                        Math.abs(eleFloor - toFloor);
                if (distance < minDistance) {
                    choosedEle = ele;
                    minDistance = distance;
                    midFloor = eleFloor;
                }
            }
        }
    }
    floorEle.remove(choosedEle);
    floorEle.add(choosedEle);
}

PersonQueue中的同步块

public synchronized void addPersonNum(){
    personNum++;
}

public synchronized void subPersonNum(){
    personNum--;
}

public synchronized int getPersonNum(){
    return personNum;
}

public synchronized void addPerson(Person person) {
    personlist.add(person);
    notifyAll();
}

public synchronized Person getOnePerson() {
    if (personlist.isEmpty()) {
        return null;
    }
    Person person = personlist.get(0);
    personlist.remove(0);
    return person;
}

public synchronized ArrayList<Person> getQueue() {
    return personlist;
}

public synchronized void setEnd(boolean isEnd) {
    this.isEnd = isEnd;
    notifyAll();
}

public synchronized boolean isEnd() {
    return isEnd;
}

public synchronized boolean isEmpty() {
    if (personlist.isEmpty()) {
        notifyAll();
    }
    return personlist.isEmpty();
}

  这次作业中借鉴了上机给出的架构设计,锁主要加在PersonQueue类的操作中,同步块主要在Input、Distributor和Elevator中,主要是防止 check & modify, read & write, write&write 的线程危险,使其互斥。

三、bug分析

  很不幸,由于线程安全或粗心问题,三次作业均出了bug,下面是具体分析:

  1. hw5中,没有考虑到输出不安全问题,导致互测被测出了不按时间戳顺序输出的bug。此bug已通过自己设计输出安全类解决;

  2. hw6中,出现了重大bug————轮询,导致强测只过了一个点,没有进入互测。具体原因是hw5中的遗留问题,由于当时对线程安全还不熟悉,滥用了notifyall()方法,到处唤醒,这样虽然hw5没问题,但由于hw6中每个队列由多电梯共用,导致无任务的电梯被反复唤醒,造成了轮询,本地也没有做过相关测试,导致本地算法完全没问题,交上去却直接寄了。此bug以通过删除多余的notifyall()解决;

  hw7中,出现了有人在队列中却始终未分配的bug,寄了三个强测点。原因是调度器在分配队列中取乘客时,把队列中的乘客删除了两次(非常玄学,按理说应该没有影响,或者直接报错,但是确实造成了“丢人”的结果)。此bug已通过删掉多余的删除操作解决。

四、互测策略

  写完作业后使用数据生成器生成数据测试自己的程序,互测时直接传入自己出bug的数据随机刀,有一定的效果(说明好几个同学都犯过我之前改过的bug),最终结果我也比较满意,但是效果显然不如大佬使用评测机轰炸。

五、心得体会

线程安全

  线程安全主要在于同步块的设计和锁的使用。这几次作业我只用了最简单的synchronized方法,但还是出现了轮询等问题。要做到线程安全,首先合理要利用锁,做到互斥的同时进行信息共享,其次还要做到同步块尽量小,以提高多线程效率。

层次化设计

  三次作业基本是迭代的,除了hw7新增了调度器类,每次作业都是在上次作业基础上的继承,或是新增了一些功能,较好实现了代码复用

  让我印象较深刻的原则——SRP: 每个类的职责应该单一,最好不要承担过多的职责,每个类只负责自己的行为, 比如:

  ​ Input仅仅负责根据乘客和电梯的信息进行新建,并加入到相应队列中。

  ​ Distributor仅仅负责在总队列中拿出乘客,并根据路径将其分配到相应的自由竞争队列中。

  ​ 电梯只负责开关门, 上下人, 等待, 转向, 到达等等。

  像我hw5和hw6中,Input同时负责新建和分发,导致了可扩展性不佳,是不合理的设计。

感受和体会

  这三次作业确实耗费了我大量的思考,推翻了一个又一个的架构,虽然三次都有bug,有一次甚至还没进入互测,但确实学到了很多东西,比如多线程该怎么写,线程安全该如何保证,以及各种debug的方法,也从身边大佬学到了非常多的架构以及优化方法,过程痛苦但也收获满满。

posted @ 2022-04-30 01:28  wuhuaka  阅读(18)  评论(0编辑  收藏  举报