OO第二单元总结博客

陈思潮-22373112 学生 2024-04-19 22:36:31

第五次的整体架构

img

  • 线程情况是采取 InputThread - Schedule(Thread) - ElevatorThread 的三类线程
  • InputThread - Schedule 之间采用生产者 - 消费者模型,共用一个 waitQueue 作为请求总池。
  • Schedule根据乘客的指派的电梯进行分配

细节实现--RequestTable的上锁

由于RequestTable类在多个线程中使用,因此对这个类的任何读写操作都加了锁。此外,由于对RequestTable的任何读写操作我都在该类内完成,我的所有synchronized块全部而且仅出现在请求类中。具体来说,读写RequestTable类主要分为以下几种情况:

  • addPerson: 两类请求都要用,用于向请求队列中加入新的请求(Person)
  • popAndRemovePerson: 从主请求中pop出一个Person用于分配给电梯子请求。
  • 上面的方法全都要加锁,此外还有一些判断Map是否为空,读和写isEnd和其他一些队列情况的方法,也需要上锁。这样我的读写全部封装在类内进行,外面只需要调用这些函数即可。

并且我在RequestTbale中专门写了waitnotifyall方法,方便外面的类调用。


第六次整体架构

img

整体介绍:

  • 线程情况依然是采取 InputThread - Schedule(Thread) - ElevatorThread 的三类线程
  • InputThread - Schedule 之间采用生产者 - 消费者模型,共用一个 waitQueue 作为请求总池。
  • 由于我的Reset的策略是将部分请求重新扔回waitQueue进行分配,所以我的ElevatorThread也相当于一个生产者
  • 我的框架设计的还行,尽可能地进行了解耦(如电梯运行策略实现在 Scheduler 中,Elevator 负责存储有关的电梯物理信息,如容量、速度、方向等),各个线程各司其职。

细节实现--第一类Reset实现

Strategy判断是否reset

  • 采用的策略接收到reset后尽力跑两层
  • strategy 判断 当前是否有reset请求,将flag置为1
    • 无乘客,直接返回advice.reset()
    • 有乘客,判断cnt是否为2,为2就reset,否则进行后续判断
  • 在flag已经被置为1的情况下,arrive后,cnt++.

电梯执行Reset()

  • 先判断电梯内是否有乘客
    • 若有,先开门,调用Out方法,
      • 先让到站的乘客出电梯
      • 让剩下的乘客出电梯,将其存入名为out People的队列中。
  • 执行
synchronized (mainTable) {
    TimableOutput.println(String.format("RESET_BEGIN-%d",this.id));
    Thread.sleep(1200);
    TimableOutput.println(String.format("RESET_END-%d", this.id));
}
  • 将独属于电梯的requestTable中的请求清空到needAlloc队列中
  • 先遍历outPeople队列,将请求重新加回电梯的requestTable,只加回了reset后的容量个数的请求,其余的扔回总池子
  • needAlloc中的请求扔回总池子中
  • flag,cnt置为0

细节实现--RequestCounter类

由于我的Reset方法中,会将请求扔回总池重新分配,所以我线程的结束就不能沿用第五次的结束方法——以inputThread输入的结束时间处为各个电梯的设置endflag

  • RequestCounter采用单例模式,在InputThread线程和Elevator被调用

    • InputThread
      @Override
          public void run() {
              try {
                  int personNum = 0;
                  ElevatorInput elevatorInput = new ElevatorInput(System.in);
                  while (true) {
                     ……//获得请求,personNum++;
                  }
                  for (int i = 0; i < personNum; i++) {
                      RequestCounter.getInstance().acquire();
                  }
                  this.mainRequestTable.setEnd(true);
                  elevatorInput.close();
          }
      
    • Elevator
      public void Out() {
              ArrayList<Person> outPeople = this.paxMap.get(this.nowFloor);
              for (Person one : outPeople) {
                  TimableOutput.println(String.format("OUT-%d-%d-%d", one.getID(), nowFloor, id));
                  RequestCounter.getInstance().release();//完成请求
              }
              this.nowNum -= outPeople.size();
              this.paxMap.get(this.nowFloor).clear();
          }
      
    • RequestCounter
      public synchronized void release() {
              cnt++;
              notifyAll();
          }
          public synchronized void acquire() {
              while (true) {
                  if (cnt > 0) {
                      cnt -= 1;
                      break;
                  } else {
                      try {
                          wait();
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }
          }
      
    • 这个过程简单来说,就有点像对帐单一样,InputThread根据输入情况获得账单数personNum,当elevator完成一个请求,就递一个账单(cnt++;)并通知Inputthread进行销账(cnt--),当还没有账单递过来时,并且目前账单没有报完时Inputthread进入等待状态等待唤醒。全部解决后,Inputthread给总池子设置setEndFlag


细节实现--影子电梯类

  • 克隆,必须要深克隆,在RequestTablePersonElevator中都写了克隆的方法
  • 新建了一个ShadowElevator类,这个类的作为Elevator的子类,需要重写Elevator中的方法
  • 比较六部影子电梯输出的时间的大小,选择最小的那部电梯进行分配。

第六次作业设计的反思

  • 不应该设计在reset前尽可能多跑两层,因为可能会出现这样的情况:

    电梯将要输出一个ARRIVE时,输入线程得到该电梯的Reset请求,这样的话这个ARRIVE就不会被记入到cnt中,所以在reset前会跑出来3次

  • 在我的设计中,不应该采取把请求重新扔回总池的分配的策略,因为这样,就意味我们无法对reset的电梯进行模拟(影子电梯一次只能模拟一步电梯的运行状况),所以可供我们模拟的影子电梯的数目会变少,有的电梯可能会承载更多的请求。


第七次整体架构

img

沿用了上次的架构,调度策略换成了顺序分配


细节实现--双轿厢电梯

  • Elevator类新增属性:maxFloorminFloorisDoublesection
  • 下层电梯A的maxFloor就是缓冲层,minFloor是0层
  • 上层电梯B的maxFloor就是12层,minFloor是缓冲层

当电梯前往maxFloorminFloor时,需要检查controller中的isOccupied是否被置位。

并且双电梯不能同时进入缓冲层。


细节实现--引入controller类---管理上下电梯的冲突

  • 引入controller类---管理上下电梯的冲突

    • controller类中添加属性isOccupied用来记录缓冲层是否被占用
    • 上下电梯公用一个controller
  • 运行策略调整:

    • 电梯在前往缓冲层时需要检查controller中的isOccupied是否被置位
    • 进入缓冲层时需要将isOccupied置位
    • 离开缓冲层时需要将isOccupied还原
  • 上下电梯同时想要前往缓冲层时,会同时访问isOccupied,这就会造成线程安全问题

  • 我们解决的手段就是加对象锁

    synchronized (controller) {
        if (controller.getStatus() == false) {
          controller.setOccupied(true);
        return Advice.MOVE;
      } else {
        controller.waitForTrans();
        controller.setOccupied(true);
        return Advice.MOVE;
      }
    }
    
  • 初此以外,我们的电梯不应当在缓冲层进行wait或者over.


细节实现--双电梯的唤醒与结束

睡眠

  • 电梯线程在运行时,第一个执行的指令是判断Max是否等于min,如果相等说明是上层电梯,,调用controller.initWait();等待唤醒

     public void run() {
            try {
                while (true) {
                    if (maxFloor == minFloor) {
                        controller.initWait();
                    }
                 ……
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
     }
    

唤醒

  • 当下层电梯重置好后就调用controller的方法,将另一部电梯唤醒

    public void Reset() {
            try {
                ……
                if (thisOne instanceof DoubleCarResetRequest) {
                    resetTwo(thisOne, outPeople, needAlloc);
                    controller.notifyAnother();
               }
            } catch (InterruptedException e) {
                System.out.println(e.getMessage());
            }
     }
    

结束

    • 双电梯线程要额外注意结束的问题,当上层电梯已经被唤醒那么它结束时就会正常的结束,那么未被唤醒的电梯呢?

    • 所以下层电梯在执行Over前要经过controller通知上层电梯,同时我们要修改Strategy策略,让它执行的第一个判断就是

      public Advice getAdvice() {
              if (maxFloor == minFloor) {
                  return Advice.OVER;
              }
                    ……
      }
      

细节实现--第二类的Reset

  • 上层电梯的唤醒
  • 上层电梯的参数修改
    • 我在Schedule类中存了所有的电梯线程,当Schedule获得第二类reset请求后,会直接修改对应的上层电梯的参数
    • 将reset传给下层电梯,由下层电梯执行reset操作

线程安全问题

浅谈如何发现问题

  • 系统分析

    • 分析输出
      • receive与accept输出不在Elevator的run中
      • 其余输出在elevator的run方法中
    • 分析外部约束
      • accept后最多run两次
      • reset-begin与reset-end之间没有receive
      • reset必须在5秒内完成
    • 分析危险指令
      • wait需要外部线程唤醒(外部线程何时唤醒很重要)
  • 发现的问题一:

    • 当schedule将要输出receive指令时
    • 时间片给到elevator执行reset,执行sleep后,时间片又还给schedule执行receive
    • 解决:
      • schedule执行分配操作并输出receive是在判断resetflag后才能执行
      • elevator执行reset-begin前需要将resetflag置为1
      • 所以我们加对象锁,elevator重新置位后释放锁,schedule执行完分配并输出receive后再释放锁
  • 发现的问题二:

  • 本该在36.1秒处执行的reset,直到程序的最后才执行

  • 此前无任何有关elevator1的输出

  • 推测:elevator1 陷入wait,直到被setend()唤醒

    img

问题分析

img

解决方法

img

时序图展示

img

心得体会

  • 多线程的在思维上十分有难度,它与以往我们所习惯的顺序执行是不同的,我们不能完全按照自己的预期理想的情况去设计多线程,而是更应该多考虑线程可能会在任意步骤被中断,去执行另一个操作,我们要去思考这样的情况下会带来什么后果,以此来设置我们的同步控制块的范围。
  • 多线程bug十分难找,因为你很难复现它,你需要仔细分析已有输出的,结合输入猜测程序在哪部分出的问题,然后再去细细地盘程序的逻辑。这十分考验人。
...全文
64 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

301

社区成员

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

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