BUAA-OO-Unit2 总结

杜启嵘-22373362 学生 2024-04-20 17:21:01

目录

  • 一.第一次作业
  • 0.题目需求分析
  • 1.处理流程分析
  • 1.1 多线程协作分析
  • UML协作图
  • 1.2 电梯调度算法
  • 1.2.1 LOOK算法
  • 1.2.2.2 电梯类以及策略类的设置
  • 1.2.2.1 Elevator extends Thread
  • 1.2.2.2 Strategy
  • 1.2.2.3 RequestTable线程安全类的实现
  • 2. UML类图以及代码复杂度分析
  • 2.1 UML类图
  • 2.2 代码复杂度分析
  • 3. Bug修复与策略
  • 二.第二次作业
  • 0.题目新增需求
  • 1.处理流程分析
  • 1.1 电梯调度策略
  • 1.1.1 UML时序图
  • 1.1.2 调参算法
  • 1.2 RESET请求的实现方式
  • 1.2.1 RESET实现
  • 1.2.2 buffer设计
  • 1.3 电梯线程结束条件
  • 2. UML类图及代码复杂度分析
  • 2.1 UML类图
  • 2.2 代码复杂度分析
  • 3. Bug修复与策略
  • 三.第三次作业
  • 0.本次作业新增需求
  • 1.处理流程分析
  • 1.1 UML时序图
  • 1.2 新增RESET请求的处理
  • 1.2.1 Elevator
  • 1.2.2 Flag
  • 1.2.3 Strategy
  • 1.2.4 Dispatcher
  • 1.2.5 OutputHandler
  • 1.3 电梯线程结束的条件
  • 2. UML图以及代码复杂度分析
  • 2.1 UML图
  • 2.2 代码复杂度分析
  • 3.Bug修复与策略
  • 四.单元总结与感悟

一.第一次作业

0.题目需求分析

  • 完成乘客请求:

    • 电梯上下行
    • 开关门动作
    • 控制乘客进出电梯
  • 电梯运行基本约束

    • 可到达楼层1-11
    • 初始位置1层
    • 数量6部(编号为1-6)
    • 移动一层花费的的时间:0.4s
    • 开/关门花费的时间:0.2s
    • 乘客进出电梯不花费时间
    • 限乘人数:6人
  • 乘客只能进入和离开输入时指定的电梯,不可以进入其他电梯(后续作业有变动)

    时间戳]乘客ID-FROM-起点层-TO-终点层-BY-电梯ID
    
  • 关于性能:

    • 运行时间尽量短
    • 尽量不让请求等待过长时间
    • 减少系统的无效运行

1.处理流程分析

1.1 多线程协作分析

​ 在第五次作业中,提供的乘客请求均已指定了接收的电梯,所以我在这一次作业中并没有设计调度器(偷懒),这导致在第六次作业中需要小小地重构一下构建调度器。第一次接触多线程编程,正所谓谋定而后动,第一重要的是梳理好各个线程之间的协作关系。在第五次作业中,主要涉及到的线程为输入线程和六个电梯线程,为每个电梯设计一个等待队列,每部电梯的等待队列即为该电梯线程与输入线程的共享变量,输入线程将拿到的请求放到队列中,电梯线程从队列中拿出请求,即输入线程为生产者,电梯线程为消费者,这里选择将等待队列类实现为一个线程安全类,即为外部提供好对等待队列的操作接口,外部调用时不必考虑线程安全问题,在类内部保证线程安全问题。这里对线程之间协作的分析如下图:

UML协作图

img

  • 主类负责启动各个线程,线程之间的共享变量写到构造方法中

  • 电梯类向策略类询问接乘客的策略

  • 这里需要注意的一点是:什么时候设置电梯线程结束?

    • 当读入的请求为空时,就能说明已经没有后续请求

    • 在请求队列中设置了标记位endFlag,当读入空请求时将每个电梯中请求队列的标记位置true

    • 策略类返回OVER建议结束电梯线程:当前电梯中没有人并且请求队列标记位为true

1.2 电梯调度算法

1.2.1 LOOK算法

这里采用了hyggge学长博客中推荐的LOOK算法,该算法可表述如下(转载)

  • 首先为电梯规定一个初始方向,然后电梯开始沿着该方向运动。

  • 到达某楼层时,

    首先判断是否需要开门

    • 如果发现电梯里有人可以出电梯(到达目的地),则开门让乘客出去;
    • 如果发现该楼层中有人想上电梯,并且目的地方向和电梯方向相同,则开门让这个乘客进入。
  • 接下来,进一步判断电梯里是否有人

    。如果电梯里还有人,则沿着当前方向移动到下一层。否则,检查请求队列中是否还有请求(目前其他楼层是否有乘客想要进电梯)

    • 如果请求队列不为空,且某请求的发出地是电梯"前方"的某楼层,则电梯继续沿着原来的方向运动。

    • 如果请求队列不为空,且所有请求的发出地都在电梯"后方"的楼层上,或者是在该楼层有请求但是这个请求的目的地在电梯后方(因为电梯不会开门接反方向的请求),则电梯掉头并进入"判断是否需要开门"的步骤(循环实现)。

    • 如果请求队列为空,且输入线程没有结束(即没有输入文件结束符),则电梯停在该楼层等待请求输入(wait)。

      注意:电梯等待时运行方向不变。在我的设计中,运行方向是电梯的一个状态量而不是过程量,用来表示下一次move时的方向。当有新请求进入请求队列时,电梯被唤醒,此时电梯的运行方向仍然是电梯wait前的方向。

    • 如果请求队列为空,且输入线程已经结束,则电梯线程结束。

1.2.2.2 电梯类以及策略类的设置

1.2.2.1 Elevator extends Thread
  • 记录电梯运行的基本信息,内聚属于每一部电梯的strategy

    private final int elevatorId; // 电梯序号 1-6
        private int curNum = 0; // 乘客人数 <=6
        private int curFloor = 1; //当前楼层 1-11
        private boolean direction = true; // 当前运行方向
        private HashMap<Integer, HashSet<Person>> destMap;
        private RequestTable requestTable;
        private Strategy strategy; // 每部电梯的策略
    
    • 关于deskmap:是<到达楼层(目的地),乘客>的哈希表,在进入乘客时,从请求队列requestTable中删除乘客并加入destmap,出电梯时从destmap中判断(或者说destmap中的人数等于curNum),这里为了避免后续加入乘客时调用destMap.get(toFloor)可能为空的情况,可以考虑对这个比较复杂的数据结构进行初始化,私有方法destMapInit

      public void destMapinit() {
              this.destMap = new HashMap<>();
              for (int i = 1;i <= 11;i++) {
                  this.destMap.put(i,new HashSet<>());
              }
      }
      
  • 电梯在每次运行时询问策略类建议,依据对应的策略运行

        @Override
        public void run() {
            while (true) {
                Advice advice = strategy.getAdvice(this.elevatorId,this.curFloor,
                        this.curNum,this.direction,this.destMap);
                if (advice == Advice.OVER) {
                    break;
                } else if (advice == Advice.MOVE) {
                    move();
                } else if (advice == Advice.REVERSE) {
                    this.direction = !this.direction;
                } else if (advice == Advice.WAIT) {
                    requestTable.waitRequest();
                } else if (advice == Advice.OPEN) {
                    openAndClose();
                }
            }
        }
    
    • 返回OVER建议时break,跳出while循环,结束进程
  • 关于电梯等待

    • 当电梯中请求队列为空且并没有结束请求时等待
    • 电梯等待列表中新增乘客或设置结束位时通知电梯,不用再等了(由于这里只有两个线程共享requestTable变量,或者说线程池中等待的只有一个线程,调用notify()notifyAll()效果是相同的)
  • 其他电梯运行方法:运行时间通过Thread.sleep(time)进行模拟实现

    • 移动

      private void move() { // 移动一层时间为0.4s
              try {
                  Thread.sleep(400);
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              this.curFloor += this.direction ? 1 : -1;
              TimableOutput.println(String.format("ARRIVE-%d-%d",this.curFloor,this.elevatorId));
          }
      
    • 进入乘客

          private void in() {
              if (this.curNum == 6) {
                  return;
              }
              ArrayList<Person> people = this.requestTable.getRequestMap().get(this.curFloor);
              for (int i = 0; i < people.size();i++) {
                  Person person = people.get(i);
                  int toFloor = person.getToFloor();
                  if (this.curNum == 6) {
                      break;
                  }
                  if ((toFloor > this.curFloor && this.direction)
                          || (toFloor < this.curFloor && !this.direction)) {
                      //乘客从requestTable中转移到destMap
                  }
              }
          }
      
      • 进入乘客时要每一次都判断是否超载
      • 对于等待队列中ArrayList遍历删除的细节:如果采用正序遍历,则每次删除需要将i回退一位,否则可能有遗漏,更优雅的方式是逆序删除
    • 出乘客

          private void out() {
              HashSet<Person> people = this.destMap.get(this.curFloor);
                // 用迭代器在目的地在当前楼层的乘客进行遍历删除
          }
      
1.2.2.2 Strategy

对于策略类的实现只需要描述出LOOK算法的内容即可

对于策略类中返回的建议使用枚举类Advice封装

  public Advice getAdvice(int elevatorId,int curFloor, int curNum, boolean direction,
                            HashMap<Integer, HashSet<Person>> destMap) {
        if (canOpenForout(curFloor, destMap) || canOpenForIn(curFloor, curNum, direction)) {
            return Advice.OPEN;
        }
        if (curNum != 0) {
            return Advice.MOVE;
        }
        else {
            if (requestTable.isEmpty()) { // 队列为空
                if (requestTable.isOver()) { // 输入结束
                    return Advice.OVER;
                } else {
                    return Advice.WAIT;
                }
            } else {
                if (hasReqInOriginDirection(curFloor, direction)) {
                    return Advice.MOVE;
                } else {
                    return Advice.REVERSE;
                }
            }
        }
    }
  • hasReqInOriginDirection:在电梯当前运行同方向上还有乘客请求
1.2.2.3 RequestTable线程安全类的实现
  • 类中主要共享变量

    // <楼层序号,每个楼层发出请求的人>
    private HashMap<Integer, ArrayList<Person>> requestMap;
    // 请求数量
    private int requestNum;
    // 请求结束标记
    private boolean endFlag;
    
  • 对以上变量的读写要保证线程安全,这里我的实现是对以上变量的读写均设置为synchronized方法,值得一提的是增加请求方法和设置结束标志方法,这两个方法需要对睡着的电梯线程进行唤醒,电梯等待的原因是此时没有请求且没有结束,这两个方法会对电梯等待的条件判断造成破坏(增加请求/已经结束),这样似乎更好理解需要唤醒电梯的原因

    • addRequest()

          public synchronized void addRequest(Person person) {
              int fromFloor = person.getFromFloor();
              requestMap.get(fromFloor).add(person);
              requestNum++;
              this.notify();
          }
      
    • setOver()

          public synchronized void setOver() {
              this.notify();
              this.endFlag = true;
          }
      
  • 其他方法:判断电梯是否为空,是否结束,电梯等待,删除请求等,不一一赘述了

2. UML类图以及代码复杂度分析

2.1 UML类图

img

2.2 代码复杂度分析

img

  • 主要复杂度集中在电梯类中进入乘客和策略类中判断策略的部分,可以理解

3. Bug修复与策略

  • 主要的调试方法:输出
  • 在此次作业中强测和互测均无bug
  • 互测中房友的bug:本摆烂人交了一发样例刀中了人,没有细究原因(摆烂摊手)

二.第二次作业

0.题目新增需求

  • 乘客不再固定电梯接送,设计电梯调度策略
  • 增加RECEIVE输出,避免自由竞争策略
  • 增加RESET请求,时长1.2s,在RESET期间电梯处于静默状态(不可以开关门、移动、RECEIVE等),重置电梯

1.处理流程分析

1.1 电梯调度策略

​ 在本次作业中,需要设计将乘客分配给合适的电梯的调度器(补第五次作业偷的懒),我的设计中选择将调度器作为一个线程实现,输入线程与调度器线程交互,调度器线程与六个电梯线程交互。对于单个电梯运行的策略我保留了第五次作业的LOOK算法,对于多部电梯的分配策略,我选择了性价比较高的调参方法,性价比体现在代码量较少的同时能够拿到比较好的性能分数。UML时序图如下

1.1.1 UML时序图

img

1.1.2 调参算法

​ 所谓调参算法其实就是选取几个有关电梯的指标,给这些指标赋予合适的参数,为每部电梯计算出得分,选择得分最高的电梯进行分配。我选取的指标有电梯接到该乘客需要走的距离,电梯中人数,电梯等待队列中人数,电梯容量,电梯速度

  • 距离:这里距离的计算是不准确的,没有找出电梯运行的上确界或下确界,即没有找出电梯运行到哪里就可以转向,而是同一按照1/11处理

    private int getDistance(int fromFloor,int toFloor,int curFloor,boolean direction) {
            int distance = 0;
            int flow = (direction) ? 1 : -1;
            if ((toFloor - fromFloor) * flow > 0) { // 乘客移动方向与电梯当前移动方向相同
                if ((fromFloor - curFloor) * flow >= 0) { // 电梯沿当前方向能接到乘客
                    distance = abs(fromFloor - curFloor);
                } else {
                    if (flow == 1) {
                        distance = 20 - curFloor + fromFloor;
                    } else {
                        distance = 20 + curFloor - fromFloor;
                    }
                }
            } else {
                if (flow == 1) {
                    distance = 22 - curFloor - fromFloor;
                } else {
                    distance = curFloor + fromFloor - 2;
                }
            }
            return distance;
        }
    
  • 电梯状态:电梯状态这个参数实际上是电梯容量、电梯中人数、电梯等待队列中人数三个量经过调参得来的,可以适当增加电梯等待队列中人数的权重,避免给一部性能好的电梯分配太多乘客,这样的性能可能还不如大家都运行

        private double getState(int capacity,int curNum,int waitNum) {
            return 1.3 * capacity - 1.1 * curNum - 1.0 * waitNum;
        }
    
  • 计算得分:这里借鉴了肖灿学长的博客中的公式(千万别用线性公式)

        private double getScore(int distance,double state,double speed) {
            return (25 - distance + state - 5 * speed) / sqrt(speed);
        }
    

关于后续被卡RTLE的处理:在电梯同时reset时,这样各部电梯得分都是相同的,我的实现中会把乘客都分给第一部电梯(擂台法记录最高得分的方式),只需要在计算得分相同时,再次判断两个电梯中哪一部的等待队列中人数少并加入其中就可以解决这个问题,或者说多部电梯同时reset且得分相同的情况约等于模6实现。

 double score = calculateScore(elevator,person);
 if (score > bestScore) {
     bestScore = score;
     bestElevatorId = i;
     bestWaitNum = elevator.getWaitNum();
 } else if (score == bestScore) {
     if (elevator.getWaitNum() < bestWaitNum) {
         bestScore = score;
         bestElevatorId = i;
         bestWaitNum = elevator.getWaitNum();
     }
 }

1.2 RESET请求的实现方式

1.2.1 RESET实现

​ 对于RESET请求的实现方式我最初使用了调度器线程和电梯线程直接交互的方式,这样的实现涉及到两个线程的交互。后来从Kai_Ker大佬那里学习到一种在电梯内部让电梯自行实现RESET的方式,并在重构中实现(膜拜)。

原子类型类 AtomicReference<>

​ 使用原子类型类AtomicReference<ResetRequest>,可以保证对于该类访问的线程安全性。这里设计ResetRequest为调度器线程和电梯线程的共享对象,在调度器中以数组形式管理六部电梯的六个ResetRequest变量。下面梳理一下处理流程:

​ 在输入线程InputThread中,构建乘客总请求表mainRequestTable,负责保管所有的乘客请求,如果拿到重置请求直接调用调度器进行处理

             while (true) {
                Request request = elevatorInput.nextRequest();
                if (request == null && dispatcher.resetOver()) {
                    mainRequestTable.setOver();
                    break;
                } else if (request == null) {
                    mainRequestTable.waitRequest();
                } else if (request instanceof PersonRequest) {
                    // 构建乘客实例
                    mainRequestTable.addRequest(person);
                } else if (request instanceof ResetRequest) {
                    this.dispatcher.resetElevator((ResetRequest) request);
                }
            }

​ 在调度器线程Dispatcher中,有管理六部电梯的Reset请求的数组ArrayList<AtomicReference<ResetRequest>> elevatorResets,当调度器拿到重置请求,就把重置请求放进数组中对应的位置,并且要唤醒对应的电梯线程,因为电梯线程此时可能处于WAIT。而电梯线程中保管着一个ResetRequest(对应着调度器线程数组中的一个元素),电梯每次运行时检查一下自己的ResetRequest是否不为空,即调度器线程是否在数组对应位置放了充值请求,如果有就先处理重置请求。

  • 调度器线程

        public void resetElevator(ResetRequest request) {
            this.elevatorResets.get(elevatorId - 1).set((ResetRequest)request);
            synchronized (this.elevatorRequestTableList.get(elevatorId - 1)) {
                this.elevatorRequestTableList.get(elevatorId - 1).notify();
            }
        }
    
  • 电梯线程

        public void run() {
            while (true) {
                if (this.resetRequest.get() != null) {
                    this.reset();
                    continue;
                }
                //获得建议并运行...
            }
        }
    

​ 在电梯拿到reset请求后,重置自己的速度和容量属性。题目中要求在输出RESET-ACCEPT之后移动楼层不超过两层(不输出超过两次ARRIVE),似乎这里还有优化空间,例如让电梯尽力走两层多送些人,但是我并没有针对这一点作出设计,而是拿到RESET请求就执行。当拿到RESET请求后,先判断电梯中此时是否有人(curNum),如果有人就要把人清空,和等待队列中的人一起扔回总请求表重新调度(这里扔回总请求表重新调度的性能应当优于放回电梯自身的等待队列,因为重置之后该电梯不一定就是得分最高,同时应当注意扔回去的请求要改变起始楼层fromFloor)。这里唯一算作优化的一点是,在重置时,可以判断一下电梯中是否有人在重置时的楼层下电梯,避免已经完成了运送又重新调度。

private void reset() {
        speed = resetRequest.get().getSpeed();
        capacity = resetRequest.get().getCapacity();
        // 如果电梯中有人
        if (curNum != 0) {
            // OPEN-CLOSE 如果目的地不是当前楼层扔回buffer
        }
        TimableOutput.println(String.format("RESET_BEGIN-%d",elevatorId));
        try {
            Thread.sleep(1200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        TimableOutput.println(String.format("RESET_END-%d",elevatorId));
        // 需要注意的是在输出之后扔回 避免提前receive
        requestTable.receiveResetRequest(buffer);
        mainRequestTable.receiveResetRequest(requestTable.getRequestList());
           // 需要注意的是最后才能set null 防止主请求表提前结束
        resetRequest.set(null);
    }
  • 这里同样需要注意的一点是,输出RESET,扔回总表,set(null)的顺序
  • 关于buffer的设计后面介绍

receiveResetRequest方法中,要注意唤醒对应的线程。我们知道,当输入线程拿到的请求为空但但电梯线程不能结束时输入线程和调度器线程会陷入WAIT状态,当电梯等待队列中没有请求且没有被设置结束时陷入WAIT状态。当我们扔回请求时,破坏了等待条件中的没有请求,需要唤醒线程再次进行确认

    public synchronized void receiveResetRequest(ArrayList<Person> resetRequest) {
        this.requestList.addAll(resetRequest);
        resetRequest.clear(); // 每次扔回之后需要清空
        this.notify();
    }

1.2.2 buffer设计

​ 题目中要求,在RESET过程中,电梯处于静默状态,不能输出RECEIVE等。一种比较简单的想法是,在分配乘客时,如果电梯处于RESET中,就不给他分配乘客。但是一种典型的情况是,六部电梯同时RESET,并在RESET期间输入大量乘客请求,以上做法很容易发生分配策略不佳导致超时RTLE或不断查询电梯状态的CTLE问题。一种较好的的解决办法是:即使电梯处于reset过程中,照样给电梯分配请求,具体的实现需要为电梯中请求队列之外新增一个缓冲队列buffer,调度器将乘客分配到电梯的请求队列中,每次电梯运行时,再从请求队列中将请求移动到缓冲队列中,这时统一输出RECEIVE

// Elevator.java
public void run() {
    while (true) {
        if (this.resetRequest.get() != null) {
            this.reset();
            continue;
        }
        // 将调度器与电梯的共享队列中的请求移动到buffer中并输出 每一次调用后requestTable一定为空
        moveRequestToBuffer();
        // 获得建议并运行
    }
}

public void moveRequestToBuffer() {
    while (!requestTable.isEmpty()) {
        Person person = requestTable.getOneRequestAndRemove();
        buffer.add(person);
        TimableOutput.println(String.format("RECEIVE-%d-%d",
                person.getId(),elevatorId));
    }
}

1.3 电梯线程结束条件

​ 这次的作业中,设置线程的结束实际上是一个链式的过程,可如下图描述

img

​ 所以重要的是考虑好起爆点的设计,即在输入线程中主请求表mainRequestTable的结束会导致调度线程和电梯线程的结束。我们知道,电梯的RESET请求会将乘客请求扔回mainRequestTable,即电梯相当于mainRequestTable的生产者,故mainRequestTable**结束条件为拿到的请求为空并且所有的RESET请求都处理完(reset数组中每个元素都为空)**。

// InputThread
public void run() {
        try {
            ElevatorInput elevatorInput = new ElevatorInput(System.in);
            while (true) {
                Request request = elevatorInput.nextRequest();
                if (request == null && dispatcher.resetOver()) {
                    mainRequestTable.setOver();
                    break;
                } else if (request == null) {
                    mainRequestTable.waitRequest();
                } else if (request instanceof PersonRequest) {
                   //...
                } else if (request instanceof ResetRequest) {
                    this.dispatcher.resetElevator((ResetRequest) request);
                }
            }
            elevatorInput.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
//Dispatcher
    public void run() {
        while (true) {
            if (mainRequestTable.isEmpty() && mainRequestTable.isOver()) { 
                for (RequestTable requestTable : this.elevatorRequestTableList) {
                    requestTable.setOver();
                }
                break;
            }
            Person person = mainRequestTable.getOneRequestAndRemove();
            if (person == null) {
                mainRequestTable.waitRequest(); //需要wait 不然会CTLE
            }
            // 分配...
        }
    }

    public boolean resetOver() {
        for (AtomicReference<ResetRequest> resetRequest : this.elevatorResets) {
            if (resetRequest.get() != null) {
                return false;
            }
        }
        return true;
    }
  • 这里mainRequestTable.isEmpty() && mainRequestTable.isOver()即为电梯处理RESET中先扔回请求再对ResetRequest置空的原因,防止置空之后请求还没扔回来的时候就错判结束了电梯线程

2. UML类图及代码复杂度分析

2.1 UML类图

img

2.2 代码复杂度分析

img

  • 代码复杂度主要聚集在策略类中判断策略的部分和电梯的动作in以及reset等,和上次作业一样

3. Bug修复与策略

  • 强测中未出现bug,互测中被卡了RTLE,通过小规模重构改正,重构后架构如前文所述
  • 关于CTLE:代码中出现轮询的问题大概是线程的run方法中while循环的问题,可以通过打印输出发现,只需要设置适当的条件使得线程阻塞而非始终循环

三.第三次作业

0.本次作业新增需求

  • 新增一种RESET请求,可以将单部电梯重置为双轿电梯
  • [时间戳]RESET_ACCEPT-电梯ID-换乘楼层-每个轿厢的满载人数-每个轿厢移动一层的时间(单位s)
  • 重置电梯两个轿厢的参数相同(满载人数、移动一层的时间)
  • 重置完成后轿厢A默认在换乘楼层的下一层,轿厢B默认在换乘楼层的上一层
  • 轿厢A只能在换乘楼层及以下运行,轿厢B只能在换乘楼层及以上运行
  • 两个轿厢不能同时处于换乘楼层
  • 特别地,双轿厢电梯可以不受RECEIVE约束地从换乘楼层移动一层以离开换乘楼层
  • 保证双轿厢电梯不会接收到第一类重置请求和第二类重置请求
  • 换乘楼层在3层和9层之间
  • 双轿厢电梯耗电量为$\frac 1 4$

1.处理流程分析

​ 本次作业中主要的任务即为处理新增的RESET请求,对于上次作业已有的调度策略没有进行改变,目标比较明确。下面是时序图

1.1 UML时序图

img

1.2 新增RESET请求的处理

1.2.1 Elevator

​ 本次作业中对于双轿厢RESET请求的类协作与第二次作业中普通重置请求相同。这里我对于RESET请求的处理方式是接收到双轿厢RESET请求时新建一个线程,将原来的线程作为A轿厢,新建的电梯线程作为B轿厢。这里首先给出电梯新增的几个属性

//Elevator
private char elevatorType = 'C'; // 轿厢类型 A B
private int transferFloor = -1;
private int lowerLimit = 1;
private int upperLimit = 11;
private Flag busyFlag = null; // 一组电梯共享一个flag进行通信
private AtomicInteger personSatisfied;
  • elevatorType:电梯的类型,初始时电梯的类型为C类型,双轿厢重置后修改为A/B类型
  • lowerlimit/upperlimit:电梯的运行楼层限制,用于Dispatcher中对于符合运行范围电梯的筛选
  • transferFloor:换乘楼层(似乎没用,不是lowerlimit就是upperlimit)
  • busyFlag:用于控制双轿厢电梯在换乘楼层互斥的需求,这里借鉴了讨论区中实现线程安全类的思路
  • personSatisfied:计数器,所有电梯线程和输入线程的共享变量,当一个乘客需求被满足(送到指定楼层),该原子类型+1

​ 参考NormalReset在第二次作业中的实现方式,DoubleCarReset我选择相同的实现方法,当电梯拿到DoubleCarReset请求之后立即进行重置

// Elevator
    private void doubleCarReset() {
        if (curNum != 0) {
            removePeopleInElevator();
        }
        OutputHandler.printResetBegin(elevatorId);
        try {
            Thread.sleep(1200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        OutputHandler.printResetEnd(elevatorId);
        // 创建新电梯线程
        //...
        newelevator.start();
        // 加入与Dispatcher共享变量中
        this.elevatorList.get(elevatorId - 1).add(newelevator);
        this.elevatorRequestTableList.get(elevatorId - 1).add(newRequestTable);
        // reset后全部扔回,防止在reset期间分配到不符合reset后电梯运行范围的乘客请求
        this.requestTable.receiveResetRequest(buffer);
        this.mainRequestTable.receiveResetRequest(this.requestTable.getRequestList());
        doubleCarResetRequest.set(null);
    }
  • 注意输出,启动新电梯线程,扔回乘客请求,set(null)的顺序,这里不再赘述
  • 这里建立新线程之后将新线程的Elevator和RequestTable加入到与Dispatcher共享的电梯表和电梯请求表中,便于获取电梯状态计算得分以及分配乘客请求。
  • 需要注意的是在电梯重置过程中调度器还在进行分配,可能会出现分配的乘客不满足RESET后电梯运行范围的问题,在我的实现中,将原电梯的等待队列变为A轿厢的等待队列,这样如果分配了位于transferFloor之上的请求就会出现错误,所以我的实现是在第二类RESET之后将给他分配的所有请求扔回去重新调度(更加精确的做法似乎是判断一下范围,将不符合范围的请求扔回去,但是感觉不如直接扔回去简洁且没有bug)

​ 在电梯移动过程中,需要注意利用busyFlag占领和释放换乘楼层,或者说,当移动到换乘楼层时进行占领(occupy),离开换乘楼层时进行释放(release)

    private void move() {
        try {
            Thread.sleep((long) (1000 * speed));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        int pace = (direction) ? 1 : -1;
        curFloor += pace;
        if (elevatorType != 'C' && curFloor == transferFloor) {
            busyFlag.setOccupied();
        }
        OutputHandler.printArrive(elevatorType,curFloor,elevatorId);
        if (elevatorType != 'C' & curFloor - pace == transferFloor) {
            busyFlag.setRelease();
        }
    }

​ 需要修改电梯中出乘客的out方法,我们可以将需要出电梯的乘客分为两类

  • 到达目的地
  • 还没到达目的地但已经到达换乘楼层:扔回总请求队列重新调度
    private void out() {
        // 到达目的地的人出电梯
        if (!destMap.get(curFloor).isEmpty()) {
            arriveDestOut();
        }
        // 需要换乘的人出电梯
        if (elevatorType != 'C' && curFloor == transferFloor) {
            transferOut();
        }
    }

1.2.2 Flag

​ 这里借鉴了讨论区同学的思路(膜拜),我的理解是,实际上是相当于对换乘楼层上了锁,一个轿厢到达换乘楼层时上锁,若锁被另一个轿厢占用则等待不输出,直到另一个轿厢释放锁,该轿厢占有锁,输出ARRIVE,这样在保证正确性的同时又保证了性能(再次膜拜)

public class Flag {
    enum State { BUSY, IDLE }

    private State state;

    public Flag() {
        this.state = State.IDLE;
    }

    public synchronized void setOccupied() {
        waitRelease(); // 两个轿厢共享一个flag 相当于对这=换乘楼层的访问进行了上锁 一个走另一个才能访问
        this.state = State.BUSY;
        notifyAll();
    }

    private synchronized void waitRelease() {
        notifyAll();
        while (this.state == State.BUSY) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public synchronized void setRelease() {
        this.state = State.IDLE;
        notifyAll();
    }
}

1.2.3 Strategy

  • 在策略类中需要新增一种建议TRANSFER,当一个轿厢要在换乘楼层WAIT或OVER时先进行TRANSER,即转向离开换乘楼层,防止另一个轿厢不能到达换乘楼层导致死锁
if (buffer.isEmpty()) { // 当前的缓冲队列为空
    if (requestTable.isOver() && requestTable.isEmpty()) {
        if (curFloor == transferFloor) {
            return Advice.TRANSFER;
        } else {
            return Advice.OVER;
        }
    } else {
        if (curFloor == transferFloor) {
            return Advice.TRANSFER;
        } else {
            return Advice.WAIT;
        }
    }
}

1.2.4 Dispatcher

​ 在调度器中,并没有对调参算法进行改进,只需要对电梯进行筛选,不能分配给运行范围不符合乘客需求的电梯。筛选的标准即为电梯能接到乘客

  • 若乘客上行,则电梯的lowerlimit应当小于等于乘客起始楼层fromFloor,电梯的upperlimit应当大于fromFloor
  • 若乘客下行,则电梯的upperlimit应当大于等于乘客起始楼层fromFloor,电梯的lowerlimit应该小于fromFloor
 for (Elevator elevator : doubleElevator) {
                if (direction && elevator.getLowerLimit() > fromFloor
                        || direction && elevator.getUpperLimit() <= fromFloor) {
                    continue;
                } else if (!direction && elevator.getUpperLimit() < fromFloor
                        || !direction && elevator.getLowerLimit() >= fromFloor) {
                    continue;
                }
                double score = calculateScore(elevator,person);
                //...
            }

1.2.5 OutputHandler

​ 由于本次作业中输出时需要判断电梯的类型,如果在每次输出时进行判断,则显得过于臃肿,故考虑到构建一个输出类,利用类中的静态输出方法区分电梯类别输出,例如:

    public static void printOpen(char elevatorType,int curFloor,int elevatorId) {
        switch (elevatorType) {
            case 'A':
                TimableOutput.println(String.format("OPEN-%d-%d-A",curFloor,elevatorId));
                break;
            case 'B':
                TimableOutput.println(String.format("OPEN-%d-%d-B",curFloor,elevatorId));
                break;
            case 'C':
                TimableOutput.println(String.format("OPEN-%d-%d",curFloor,elevatorId));
                break;
            default:
                break;
        }
    }

1.3 电梯线程结束的条件

​ 在这次作业中,由于换乘乘客也需要扔回去重新调度,加上两种RESET请求,设置结束的条件更加复杂。最初我写了各种“旁敲侧击”的条件,例如各种队列是否为空,但是效果并不好,总是出现电梯线程提前结束的情况。其实关于电梯线程结束最直接的条件就是一定要满足人民对于美好生活的需求即判断是不是所有的乘客都已经到达了目的地,是不是所有的两类reset请求都已经完成。

  • 是不是所有乘客都已经到达了目的地
    • 设置一个全局共享计数器,personSatisfied,每当一个乘客到达目的地,+1
    • 在输入线程中设置一个乘客计数器,personCnt,每拿到一个乘客请求,+1
  • 是不是两类reset请求都已经完成
    • 遍历两个数组中是不是每个元素都是空
//InputThread
    public void run() {
        try {
            ElevatorInput elevatorInput = new ElevatorInput(System.in);
            int personCnt = 0; // 计算乘客数量
            while (true) {
                Request request = elevatorInput.nextRequest();
                if (request == null &&
                        (personCnt == personSatisfied.get()) && (dispatcher.ResetOver())) {
                    mainRequestTable.setOver();
                    break;
                } else if (request == null) {
                    mainRequestTable.waitRequest();
                } else if (request instanceof PersonRequest) {
                    //...
                    personCnt++;
                } else if (request instanceof NormalResetRequest) {
                    this.dispatcher.normalResetElevator((NormalResetRequest) request);
                } else if (request instanceof DoubleCarResetRequest) {
                    this.dispatcher.doubleCarResetElevator((DoubleCarResetRequest) request);
                }
            }
            elevatorInput.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
//Dispatcher
    public boolean ResetOver() {
        for (AtomicReference<DoubleCarResetRequest> doubleCarResetRequest :
                this.doubleCarElevatorResets) {
            if (doubleCarResetRequest.get() != null) {
                return false;
            }
        }
        for (AtomicReference<NormalResetRequest> normalResetRequest :
                this.normalCarElevatorResets) {
            if (normalResetRequest.get() != null) {
                return false;
            }
        }
        return true;
    }

2. UML图以及代码复杂度分析

2.1 UML图

img

2.2 代码复杂度分析

Dispatcher.calculateScore(Elevator, Person)0.01.01.01.0
Dispatcher.Dispatcher(RequestTable, ArrayList>, ArrayList>, ArrayList>, ArrayList>)0.01.01.01.0
Dispatcher.dispatchRequest(Person)23.05.014.015.0
Dispatcher.doubleCarResetElevator(DoubleCarResetRequest)0.01.01.01.0
Dispatcher.doubleCarResetOver()3.03.02.03.0
Dispatcher.getDistance(int, int, int, boolean)13.01.03.06.0
Dispatcher.getScore(int, double, double)0.01.01.01.0
Dispatcher.getState(int, int, int)0.01.01.01.0
Dispatcher.normalResetElevator(NormalResetRequest)0.01.01.01.0
Dispatcher.run()13.04.07.07.0
Elevator.arriveDestOut()1.01.02.02.0
Elevator.destMapinit()1.01.02.02.0
Elevator.doubleCarReset()2.01.03.03.0
Elevator.Elevator(int, RequestTable, RequestTable, AtomicReference, AtomicReference, ArrayList>, ArrayList>, ...)0.01.01.01.0
Elevator.elevatorAinit(int, Flag, double, int)0.01.01.01.0
Elevator.elevatorBinit(int, Flag, double, int)0.01.01.01.0
Elevator.getCapacity()0.01.01.01.0
Elevator.getCurFloor()0.01.01.01.0
Elevator.getCurNum()0.01.01.01.0
Elevator.getDirection()0.01.01.01.0
Elevator.getElevatorType()0.01.01.01.0
Elevator.getLowerLimit()0.01.01.01.0
Elevator.getSpeed()0.01.01.01.0
Elevator.getUpperLimit()0.01.01.01.0
Elevator.getWaitNum()0.01.01.01.0
Elevator.in()12.04.04.09.0
Elevator.move()5.01.04.06.0
Elevator.moveRequestToBuffer()3.03.02.03.0
Elevator.normalReset()2.01.03.03.0
Elevator.openAndClose()1.01.02.02.0
Elevator.out()3.01.03.04.0
Elevator.removePeopleInElevator()8.01.05.05.0
Elevator.run()12.05.010.010.0
Elevator.transfer()4.01.01.05.0
Elevator.transferOut()12.01.06.06.0
Flag.Flag()0.01.01.01.0
Flag.setOccupied()0.01.01.01.0
Flag.setRelease()0.01.01.01.0
Flag.waitRelease()3.01.03.03.0
InputThread.InputThread(RequestTable, Dispatcher, AtomicInteger)0.01.01.01.0
InputThread.run()9.03.010.010.0
Main.main(String[])1.01.02.02.0
OutputHandler.printArrive(char, int, int)1.01.01.04.0
OutputHandler.printClose(char, int, int)1.01.01.04.0
OutputHandler.printIn(char, int, int, int)1.01.01.04.0
OutputHandler.printOpen(char, int, int)1.01.01.04.0
OutputHandler.printOut(char, int, int, int)1.01.01.04.0
OutputHandler.printReceive(char, int, int)1.01.01.04.0
OutputHandler.printResetBegin(int)0.01.01.01.0
OutputHandler.printResetEnd(int)0.01.01.01.0
Person.getFromFloor()0.01.01.01.0
Person.getId()0.01.01.01.0
Person.getToFloor()0.01.01.01.0
Person.Person(Integer, Integer, Integer)0.01.01.01.0
RequestTable.addRequest(Person)0.01.01.01.0
RequestTable.delRequest(Person)0.01.01.01.0
RequestTable.getOneRequestAndRemove()1.02.01.02.0
RequestTable.getRequestList()0.01.01.01.0
RequestTable.isEmpty()0.01.01.01.0
RequestTable.isOver()0.01.01.01.0
RequestTable.receiveResetRequest(ArrayList)0.01.01.01.0
RequestTable.RequestTable()0.01.01.01.0
RequestTable.setOver()0.01.01.01.0
RequestTable.waitRequest()1.01.02.02.0
Strategy.canOpenForIn(int, int, boolean, int)14.05.03.08.0
Strategy.canOpenForout(int, int, char, HashMap>)19.09.07.09.0
Strategy.getAdvice(int, int, char, int, boolean, int, HashMap>)26.08.05.010.0
Strategy.hasReqInOriginDirection(int, boolean)6.03.02.06.0
Strategy.Strategy(RequestTable, ArrayList)0.01.01.01.0
  • 代码复杂度主要集中在调参算法分配策略、电梯的动作、策略类中

3.Bug修复与策略

  • 本次作业中强测和互测均未出现bug
  • 房友出现RTLE问题

四.单元总结与感悟

  这一单元作业完成过程体感上没有第一单元顺利,第二单元debug的过程说实话挺折磨的,从第一次接触多线程编程到最后逐渐得心应手,在一行一行的代码编写中收获了自我的成长才是难能可贵的,在困难中一步步提升自己,苦思冥想架构设计,一行行构建自己的架构,这也许正是OO课程的魅力!
...全文
69 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

发帖
与我相关
我的任务
社区描述
2023年北航面向对象设计与构造
学习 高校
社区管理员
  • YannaZhang
  • CajZella
  • C_ecelia
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧