OO 第二单元总结

赵泽文-22371150 赵泽文-22371150 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要 + 加入社区

博客撰写基本要求:

OO 第二单元总结

概述

本单元设定了一个电梯接送乘客、模拟电梯重置和"分身"的场景,涉及多线程、线程安全、共享资源及其锁的使用等许多问题,相比于上个单元,在架构和实现方式、debug、测试和 bug 复现、性能优化上,都有许多不同.

回顾一下这个单元的三次作业,可以发现是一个逐渐深入,愈发复杂的过程.

hw5 是在请求中指定了电梯分配,主要目的是让同学们对线程的基本操作有所了解,并解决电梯对于一个队列应当如何接送乘客的问题,为这个单元后面多电梯调度打好框架。

hw6 中取消了请求中对电梯分配的指定,并新增了基础的重置操作,主要解决多电梯调度问题.

hw7 中又新增了一个双轿厢电梯重置请求,使每一个"电梯槽"里面也应该有两个线程,并且需要解决在换乘层上下两个电梯冲突的问题。

作业架构迭代

hw5


本次作业中, 我只开了两种线程, InputThread 和 Elevator. 因为电梯都分配好了, 没必要写调度器.
InputThread 直接将读到的请求分配给相应电梯上的 requestTable
我的 Scheduler 类事实上并不是调度器,而是一个内部策略类. 所以后面我直接将它重命名为 Stratrgy 了.
策略采用 Look 算法, 简单有效.

在 MainClass 中创建所有线程一起 start.

这样,我的结构第一次比较简单,甚至简陋,后面改起来就比较麻烦

hw6

本次作业

uml

hw6

时序图

本次作业新增 reset, 并且需要完成调度器设计.
吃了第一次的亏, 重构过程一波三折.

  1. 重命名原来的 Scheduler 为 Strategy, 并新增线程 Scheduler 用于调度.

  2. 本来 waitQueue 想要复用 requestTable, 但是发现 resetRequest 放进 requestTable 并不合适. 所以又新建了一个类. 如果最开始设计好,完全可以写成继承.

  3. 关于 reset: resetRequest 与 personRequest 一同进入 waitQueue, 然后由调度器分配给相应的电梯中的 requestTable. 我一直对此有所担心, 因为 resetRequest 无法在 accept 的第一时间给到电梯. 所以我是让电梯收到 reset 就直接就近停下, reset. 并不贪图再走 1 层.
    reset 接受后到完成 过程中, 置位 isReset, 在这个过程中, 依然会向其分配请求, 但是并不输出 -- 而是将其分配到一个缓冲队列当中, 等到 reset 完成时再正式 receive.
    reset 过程要开门下客, 下来的请求需打回重新调度. 那么 inputThread 结束, 就不能直接作为 Scheduler 结束 的标志, 而应该与上所有 的电梯都没有正在 reset.

    
    
  • 调度策略
    我一开始考虑的调度策略是选取几个参数比较, 但是写出来以后性能一言难尽, 所以还是直接选择了 random.

hw 7

我中测没过, 很痛苦. 但还是大概讲讲我的思路吧

新增兄弟电梯.

-

  • 调度策略: 依然是 random. 如果有兄弟电梯, 则一对电梯 A 和 B 共享 1/6 的概率:
    具体而言是这样:
private Elevator randomElevator(int fromFloor) {
    int randomNumber = (int) (Math.random() * 6) + 1;
    Elevator elevatorA;
    Elevator elevatorB;
    synchronized (elevators) {
        elevatorA = elevators.get(randomNumber);
        elevatorB = elevators.get(randomNumber + 6);
        if (elevatorA.isDc()) {
            if (fromFloor < elevatorA.getTransFloor()) {
                return elevatorA;
            } else {
                return elevatorB;
            }
        }
    }
    return elevatorA;
}
  • 关于换乘层:
    有一个很简单但是我一开始没有注意到的问题: 到达换成层, 所有的乘客都要下车, 下来以后, 也要回到 waitQueue 重新分配.

这一点表明, Scheduler 的结束条件应当再加上一条,也就是没有电梯正在换乘层下客.
我虽然后面想到这一点, 但是对其的解决不够彻底, 出现了好多乘客下去以后, 还没有被重新分配, 就已经结束了线程, 有时还会出现 tle 的情况(死锁).

此外我还粗心大意地将换乘层下车的乘客请求原封不动地打回 waitQueue, 忘记用 curFloor 代替 fromFloor 了.

  • 关于分裂时线程的启动:
    在 Elevator 中 将自己的 isA, isDc 等属性改变, 变成 elevatorA;然后, 实例化兄弟电梯 B, 最后调用 Scheduler 的addElevator方法, 在 addElevator 方法里 start 新的电梯线程.
    这里涉及 elevators 的线程安全问题, 所以我就开始疯狂打补丁, 在使用 elevators 的地方 加锁.

其它一些关键点

如何实现双轿厢的两个轿厢不碰撞

感谢讨论区佬的课堂, 直接学习下来. 用一个 Flag 信号量, 保证兄弟电梯不会同时进入换乘层.
PV 大法真万能.

Elevator.java

private void move() {
    try {
        sleep(moveTime);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    if (isDc && currentFloor + direction == transFloor) {
        occupied.waitRelease();
    }
    currentFloor += direction;
    if (isDc && currentFloor == transFloor) {
        occupied.setOccupied();
    }
    OutputHandler.arrive(currentFloor, fullId(), elevatorId);
    if (isDc && currentFloor - direction == transFloor) {
        occupied.release();
    }
    // Arrive
}

此外还要考虑"占着内个不内个"的问题, 也就是说, 当一个电梯在换乘层时,不能随随便便就停下,甚至结束. 应当尽快驶离换乘层腾出位置来.

Strategy.java

// 在换乘层,赶紧离开
if (!elevator.isDc() && passengerCount != 0 || elevator.isDc() && curFloor == transFloor &&
        Utils.sameDirection(transFloor, elevator.isA() ? minFloor : maxFloor, direction)) {
        return Advice.MOVE;
    }

锁

先直接来一个汇总:

46 个结果 - 6 文件

src\Elevator.java:
   92
   93:     public synchronized boolean isDc() {
   94          return isDc;

   96
   97:     public synchronized void setDc(boolean dc) {
   98          isDc = dc;

  109
  110:     public synchronized int getCapacity() {
  111          return capacity;

  121
  122:     public synchronized boolean isReseting() {
  123          notifyAll();

  126
  127:     public synchronized void setReseting(boolean reseting) {
  128          this.reseting = reseting;
  129          notifyAll();
  130:         synchronized (requestTable) {
  131              requestTable.notifyAll();

  134
  135:     public synchronized boolean isToBeReset() {
  136          notifyAll();

  139
  140:     public synchronized void setToBeReset(ResetRequest resetRequest, boolean toBeReset) {
  141          this.toBeReset = toBeReset;

  143          notifyAll();
  144:         synchronized (requestTable) {
  145              requestTable.notifyAll();

  148
  149:     public synchronized Advice getCurState() {
  150          notifyAll();

  160          while (true) {
  161:             synchronized (this) {
  162                  curState = Strategy.getAdvice(this);

  187
  188:     public synchronized boolean isOpening() {
  189          return curState == Advice.OPEN && isDc && currentFloor == transFloor;

  219          }
  220:         synchronized (waitQueue) {
  221              waitQueue.notifyAll();

  303
  304:         synchronized (waitQueue) {
  305              waitQueue.notifyAll();

  330          }
  331:         synchronized (waitQueue) {
  332              waitQueue.notifyAll();

src\Flag.java:
   7
   8:     public synchronized void setOccupied() {
   9          waitRelease();

  13
  14:     public synchronized void release() {
  15          this.state = State.UNOCCUPIED;

  18
  19:     public synchronized void waitRelease() {
  20          notifyAll();

src\RequestTable.java:
   29
   30:     public synchronized HashMap<Integer, ArrayList<PersonRequest>> getRequestMap() {
   31          return requestMap;

   33
   34:     public synchronized void addRequest(PersonRequest request) {
   35          requestMap.get(request.getFromFloor()).add(request);

   39
   40:     public synchronized boolean isOver() {
   41          notifyAll();

   44
   45:     public synchronized void setOver(boolean isOver) {
   46          this.isOver = isOver;

   49
   50:     public synchronized boolean isEmpty() {
   51          notifyAll();

   55
   56:     public synchronized void waitRequest() {
   57          if (!isOver && isEmpty()) {

   75
   76:     public synchronized ArrayList<PersonRequest> deleteRequest(int currentFloor,
   77                                                                 int direction,

  101
  102:     public synchronized ArrayList<PersonRequest> deleteAllRequests() {
  103

  113
  114:     public synchronized int getRequestCount() {
  115          return requestCount;

src\Scheduler.java:
   18      public void addElevator(Elevator elevator) {
   19:         synchronized (elevators) {
   20              elevators.put(elevator.getElevatorId(), elevator);

   37                  boolean b;
   38:                 synchronized (elevators) {
   39                      b = waitQueue.isInputOver() &&

   44                  if (b) {
   45:                     synchronized (elevators) {
   46                          elevators.values().forEach(elevator -> elevator.setOver(true));

   55                      }
   56:                     synchronized (waitQueue) {
   57                          try {

   76                      ((DoubleCarResetRequest) request).getElevatorId();
   77:                 synchronized (elevators) {
   78                      elevators.get(resetId).setToBeReset((ResetRequest) request, true);

   93          Elevator elevatorB;
   94:         synchronized (elevators) {
   95              elevatorA = elevators.get(randomNumber);

  129              RequestTable waitList1 = elevator.getRequestTable();
  130:             synchronized (waitList1) {
  131                  if (Utils.sameDirection(from, to, direction) && passengerCount < capacity &&

  162          RequestTable waitList1 = elevator.getRequestTable();
  163:         synchronized (waitList1) {
  164              return Utils.sameDirection(from, to, direction) && passengerCount < capacity &&

  200          int sum = Integer.MAX_VALUE;
  201:         synchronized (elevators) {
  202              for (Elevator elevator : elevators.values()) {

  204                  RequestTable waitList1 = elevator.getRequestTable();
  205:                 synchronized (waitList1) {
  206                      int outCount = waitList1.getRequestCount();

  217      public Elevator getSibling(int id) {
  218:         synchronized (elevators) {
  219              return elevators.get((id + 6) % 12);

src\Strategy.java:
   85          // "顺路就捎":这层有人想去的方向与运动方向相同
   86:         synchronized (waitQueue) {
   87              return haveSpace && waitQueue.stream().anyMatch(personRequest -> Utils.sameDirection(

  113          requestMap = requestTable.getRequestMap();
  114:         synchronized (requestMap) {
  115              return requestMap.keySet().stream()

src\WaitQueue.java:
  13
  14:     public synchronized void addRequest(Request request) {
  15          requests.add(request);

  18
  19:     public synchronized Request getOneRequestAndRemove() {
  20          if (requests.isEmpty()) {

  28
  29:     public synchronized boolean isInputOver() {
  30          notifyAll();

  33
  34:     public synchronized void setInputOver(boolean isOver) {
  35          this.inputOver = isOver;

  38
  39:     public synchronized ArrayList<Request> getRequests() {
  40          notifyAll();

  43
  44:     public synchronized boolean isEmpty() {
  45          notifyAll();

可以看到, 我大部分锁都加在了方法上, 主要是对各种托盘的存取方法都加了锁.
但是我还是有许多地方未加以思考就滥用了锁, 导致经常出现死锁.

有几个比较失败的加锁就是对 elevator 加的锁, 这些主要是用来判断 Scheduler 结束条件的.

测试, bug 与 debug

打表的心得:

 public static void arrive(int floor, String fullId, int elevatorId) {
        TimableOutput.println(String.format(
            "%sARRIVE-%d-%s",
            Utils.getIndent(elevatorId),
            floor,
            fullId));
    }

根据 id 将输出进行缩进, 不同电梯一眼就能分开.

最大的败笔

Strategy 控制结构复杂, 导致本不是重点的 Strategy 在最后一次作业频繁出错.
if- else 非常混乱,这里一个特判,那里一个特判.

hw5.drawio 8.40K

这时候就羡慕状态机了.

hw5.plantuml 1.88K

hw5.svg 24.53K

hw5_sq.svg 17.25K

hw6.svg 38.68K

hw6_sq.svg 26.18K

hw7.svg 44.21K

hw7_.svg 39.81K

hw7_sq.svg 37.05K

...全文
评论
收藏 已收藏
分享
返回顶部