OO第二单元总结

本文详细描述了作者在编程作业中设计电梯调度系统的过程,包括没有调度器时的自由竞争策略,如何处理Reset和DcReset请求,同步块的设置,锁的选择,以及解决轿厢碰撞问题和线程安全挑战的心得体会。作者强调了理解线程安全和正确使用锁在设计中的重要性。

架构设计

调度器设计(没有调度器)

本单元作业我只有在hw_5中设置了调度器,并只有简单的按电梯id分配请求,故不再赘述
hw_6,hw_7均采用自由竞争的策略,即不实施调度,各电梯从总队列中自由获取请求

hw_5

第一次作业要求较为简单,故每个电梯都各自有一个候乘表,仅需要考虑这个表里的请求如何处理即可,所以重点关注电梯的运行策略。

以下是UML类图和协作图
在这里插入图片描述
在这里插入图片描述

  • Look策略
    Elevator.java

      while (true) {
          end();//判断是否结束
          //passengers.isEmpty() && allListEle.isEnd() && allListEle.isEmpty()
          strategy.reInitial(this);//为策略类更新电梯状态
          end();
          ups = strategy.getUps();//策略类给出要上电梯的请求
          downs = strategy.getDowns();//策略类给出要下电梯的请求
          direction = strategy.getDirection();//策略类给出电梯接下来的方向
          boolean e = false;
          if (ifOpen(ups,downs)) {//判断是否开门
              open();//开门
              off(downs);//下电梯
              on(ups);//上电梯
              strategy.reInitial(this);//再次更新状态(判断此时是否可能转向)
              direction = strategy.getDirection();
              ups = strategy.getUps();
              on(ups);//新请求||因转向而可捎带的请求,上电梯
              close();//关门
          }
          end();
          strategy.reInitial(this);
          direction = strategy.getDirection();
          end();
          move();//移动一层
      }
    
    • 我采取的是将策略类对象作为电梯的一个属性,通过与电梯在特定时间交换信息实施决策,并指挥电梯的动作。特定时间即strategy.reInitial(this)。将电梯的动作和对当前状态的分析决策分开。电梯不用关心目前有什么请求,只需要根据策略执行固定的动作。策略类也只需要把电梯传进来的信息做分析并返回决策即可。
    • 在电梯开门且上下乘客后更新电梯状态,这样可以适应,电梯内最后一个乘客下电梯后,运行方向上只有本层有反向的请求,这样在开门的时候转向,并再一次上电梯,可以避免多次开门,或者反复折返而无法接到乘客的问题。

    Srategy.java

      public void reInitial(Elevator elevator) {
          this.currentFloor = elevator.getCurrentFloor();
          this.waitingListEle = elevator.getWaitingListEle();
          this.allListEle = elevator.getAllListEle();
          this.passengers = elevator.getPassengers();
          this.direction = elevator.getDirection();
          ups.clear();
          downs.clear();
          setWait();//判断电梯是否需要等待,若需要则直接在这里等待
          setDirection();//判断是否转向
          setDowns();//决定下电梯的人
          setUps();//决定上电梯的人
      }
    

hw_6

这次作业不再限制请求要乘坐的电梯,并加入了Reset请求。
从这次作业开始,我实行自由竞争策略,故移除了Schedule,没有新增类。这次作业是改动最大的一次,相当于重构了。

以下是UML类图和协作图
在这里插入图片描述
在这里插入图片描述

  • 加入了多套锁进行维护,锁系统我将在下面同步块的设置和锁的选择标题中叙述
  • 深克隆6个allList队列,队列中包含所有类型的所有(包括其他电梯的Reset)请求,这几个队列将保证时刻同步
    • allList的所有操作,都遍历对所有6个执行
    • 为什么不用同一个:如果用同一个,新请求唤醒电梯时将只唤醒随机一个电梯,背离了自由竞争的观念,故一个电梯一个,唤醒时逐个全部唤醒
    • Input无条件加入allList,只有乘客上电梯时或Reset执行完毕前从中移除相应请求,在Reset赶出未完成的请求时,重新加入6个allList
    • allList参与构成每个电梯的结束条件,即当allList空时,可以确定所有请求处理完成(上了电梯的也不会下电梯),因为Reset请求在处理完毕前才删除,若有请求被赶出,应加入allList,保持了allList不空状态,不会提前结束
  • 每个电梯有自己的resetList队列
    • Input中来Reset请求时,应(通过锁)保证同步加入allList和相应的resetList。避免被allList唤醒却发现没有Reset请求的错误
    • 移除也同步移除
    • 电梯会定期监视,resetList不空时,电梯尽早重置
  • 将各电梯的每层请求队列floorList.get(i)合并,变为总的floorList

Reset请求的处理

  • 我将Reset请求和Person请求视为同等请求均放入allList
  • Reset的处理视为电梯的动作之一
      multiple.readLock().lock();//////////////multiple
      if (!resetList.isEmpty()) {
          multiple.readLock().unlock();////////////////multiple
          reset();//执行重置函数
          continue;//相当于回到初始状态
      }
      multiple.readLock().unlock();//////////////multiple
    
  • reset()具体实现
      如果电梯不空,“赶出”乘客;//加回allList
      "RESET_BEGIN";
      将已`Receive`的请求释放掉;
      移除Reset请求;
      sleep(1200);
      置电梯属性;
      "RESET_END";
    
  • 每当乘客被赶出时,都要执行以下代码以更新请求
      passengers.get(i).setFromFloor(currentFloor);
      passengers.get(i).setDispatched(0);
      passengers.get(i).setPrinted(false);
    

自由竞争的实现

由于本次课程组的限制,要求打出RECEIVE以鼓励实现调度器,避免陪跑,故实现自由竞争较为困难,没有RECEIVE的电梯不能移动,被RECEIVE的乘客必须乘坐指定电梯。

  • 但是,考虑到有乘客打出过RECEIVE的电梯可以自由移动
    • Person请求类添加isDispatchedisPrinted属性,分别标志分配给哪个电梯(0代表未分配,k代表分给id为k-1的电梯) 和是否打印RECEIVE
    • 当乘客不空时,正常竞争运行,过程中捎带的乘客在上电梯的同时根据isPrinted决定是否打印RECEIVE
    • 乘客空时,按hw_5思路,判断是否wait,只需在wait的条件中加入一个新条件judge()
        while (judge() && !(allList.get(elevatorId - 1).isEnd() &&
                allList.get(elevatorId - 1).isEmpty())) {
            allList.get(elevatorId - 1).wait();
        }
      
  • judge()的思路
      public boolean judge() {
          遍历allList.get(elevatorId - 1){
              遇到乘客请求(isDispatched()==elevatorId + 1){
                  根据isPrinted()是否打印:
                  return false;//不等待
              }
              遇到自己的Reset请求{
                  return false;//不等待
              }
          }
          再次遍历allList.get(elevatorId - 1){
              遇到乘客请求(isDispatched()==0){
                  setDispatched(elevatorId + 1);//抢请求
                  打印RECEIVE;
                  setPrinted(true);//标记已打印
                  return false;//不等待
              }
          }
          return true;//最后得出要等待
      }
    
  • 这样,乘客空或刚被唤醒的电梯,无法移动,会去总队列中“”一个请求并打出RECEIVE,获得移动权,只要运行起来,接下来就可以实现自由竞争了

hw_7

加入第二类重置请求以及双轿厢。架构几乎没有变化,为策略和电梯实现了接口。

以下是UML类图和协作图
在这里插入图片描述
在这里插入图片描述

还有对各种请求的管理
在这里插入图片描述

DcReset请求的处理

  • 与之前的NormalReset一样将DcReset请求和其他请求视为同等请求均放入allList
  • NormalReset共同放入resetList
  • DcReset的具体处理与NormalReset相同,视为电梯的动作之一
      multiple.readLock().lock();//////////////multiple
      if (!resetList.isEmpty()) {
          multiple.readLock().unlock();////////////////multiple
          if (choose()) {
              resetNormal();
          } else {
              resetDc();
              return;//关闭电梯
          }
          continue;
      }
      multiple.readLock().unlock();////////////////multiple
    
  • resetDc()具体实现
      如果电梯不空,“赶出”乘客;//加回allList
      "RESET_BEGIN";
      将已`Receive`的请求释放掉;
      移除Reset请求;
      sleep(1200);
      置电梯属性;
      "RESET_END";
      this.elevatorA.start();//启动A轿厢
      this.elevatorB.start();//启动B轿厢
    

双轿厢电梯的实现

  • 实现Elevator接口,下接NormalElevatorDcElevator两种电梯
  • 实现Strategy接口,下接NormalStrategyDcStrategyADcStrategyB三种策略,本质都是look策略,细节处理有所差异。这样不用修改过多代码即可实现双轿厢
  • Main中创建18个电梯线程(6个normal,12个Dc),并创建好各自的策略,传入对应电梯,随即启动6个Normal电梯,其余12个等待启动
  • allList也变为18个,启动后的A、B轿厢和其他单轿厢电梯地位相同,共同竞争
  • DcElevator在每到达一层开门下完人后,判断是否为换乘楼层,若是且有乘客则赶人。注意加入allList清空乘客容器,防止两队列全空的间隙,另一个轿厢提前结束(见下一条)
  • 由于DcElevator在换乘楼层赶人增添新请求,影响到其他电梯的结束,故我让其另一个轿厢监管这个请求,防止请求被遗弃,即将DcElevator的结束条件新增一项
    fellow.getPassengers().isEmpty()//另一个轿厢乘客空
    
  • 然后就是一些对DcStrategy细节上的修改,不赘述,如何防撞在下面专题介绍

同步块的设置和锁的选择

由于后两次作业采用了自由竞争的策略,故线程安全较难保证,于是我设置了较为复杂的锁系统,自认为还算安全,但由于锁太多,等待锁的时间较长,所以最后的性能分也不算很高,但也很不错了。

类型作用
multipleReentrantReadWriteLock保证18个allList,floorList以及resetList的同步修改
allList.get(i)对象锁保证在遍历某个allList时,其不能被改变
rwLockReentrantReadWriteLock共有11个,维护11个每层的请求队列floorList.get(i)
personLockReentrantLock维护Person请求中isPrinted,isDispatched属性
DcLockReentrantLock防止轿厢碰撞
  • 锁的嵌套关系 (由外到内)
    multiple > allList.get(i) > personLock
    multiple > rwLock > personLock
    allList.get(i) > DcLock
    
    • 严格按照这样的嵌套关系可以避免死锁
  • 读写锁可以省去读-读等待锁的时间
  • personLock是电梯自由竞争的关键,能避免多个电梯同时抢到同一乘客,优先抢到锁的电梯才能修改请求的isDispatched属性为自己的标志
  • 下面举两个我代码中的例子
    Input.java
      if (request instanceof NormalResetRequest) {//读入新的Reset请求
          Reset reset = new NormalReset(*);
          multiple.writeLock().lock();//保证18个`allList`和`resetList`的同步修改
          for (int i = 0; i < 18; i++) {
              allList.get(i).addRequest(reset);
          }
          resetList.get(((NormalResetRequest) request).getElevatorId() - 1).addRequest(reset);
          multiple.writeLock().unlock();
    
    NormalElevator.java
      public void on(ArrayList<Person> ups) {//上电梯
          if (!ups.isEmpty()) {
              for (int i = 0; i < ups.size() && passengers.size() < capacity; i++) {
                  if (!ups.get(i).getPrinted()) {
                      TimableOutput.println(String.
                              format("RECEIVE-%d-%d", ups.get(i).getPersonId(), id));
                  }
                  TimableOutput.println(String.
                          format("IN-%d-%d-%d", ups.get(i).getPersonId(), currentFloor, id));
                  passengers.add(ups.get(i));//不共享,不用维护
                  multiple.writeLock().lock();//保证18个`allList`和`floorList`的同步修改
                  writeLockListOn.get(currentFloor - 1).lock();//维护floorList.get(currentFloor - 1)
                  for (int k = 0; k < 18; k++) {
                      allList.get(k).removeRequest(ups.get(i));
                  }
                  floorList.get(currentFloor - 1).removeRequest(ups.get(i));
                  writeLockListOn.get(currentFloor - 1).unlock();//释放
                  multiple.writeLock().unlock();//释放
              }
          }
      }
    

check-then-act

本单元作业中涉及多处check-then-act线程安全隐患,研讨课上有同学分享并没有维护也没有出问题,的确这种问题出现bug的几率极小,但我认为也是必须要维护的

  • 例如:电梯等待的条件
    synchronized (allList.get(elevatorId - 1)) {
        while (judge() && !(allList.get(elevatorId - 1).isEnd() && allList.get(elevatorId - 1).isEmpty())) {
            allList.get(elevatorId - 1).wait();
        }
    }
    //////////////比较
    while (judge() && !(allList.get(elevatorId - 1).isEnd() && allList.get(elevatorId - 1).isEmpty())) {
        synchronized (allList.get(elevatorId - 1)) {
            allList.get(elevatorId - 1).wait();
        }
    }
    
    • 上述两种写法,显然上面的是线程安全的,下面的代码刚进行完while判断,判断为真后,很可能其值被修改为假,然而此时已不可避免的执行wait(),导致最后无法被唤醒;还有可能在多条件判断时,前面条件判断时,后面的条件可能会被修改,造成误判。
    • 但是事实上,上面的写法运行时间会长于下面,因为临界区域大,但这是必要的,所以我认为本单元作业,最重要的不是去卷性能,意义不大,最重要的是更深刻的理解和掌握线程安全,这对未来的帮助很大。

轿厢碰撞问题

我的设计是:

  • 每当Dc电梯即将前往换乘楼层时,询问另一轿厢是否正处在换乘楼层,是则等待,否则正常运行
  • 每当Dc电梯正常移动前,判断是否处在换乘楼层,是则唤醒另一轿厢(无论其是否在wait中)
  • 每当Dc电梯因缺乏请求而即将wait()前,先强行将其移离换乘楼层,随后唤醒另一轿厢,再wait(),防止占用换乘楼层进行等待

具体实现则用到了上面提到的DcLock
DcElevator.java - move()

//例,此时若方向向上
synchronized (lock) {//DcLock
    if (type == 'A' && currentFloor == transferFloor - 1 && fellow.getCurrentFloor() == transferFloor) {
        try {
            lock.wait();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
    sleep(*);
    TimableOutput.println(String.format("ARRIVE-%d-%d-%c", currentFloor + 1, (id - 5) / 2, type));
    currentFloor++;
    if (type == 'B' && currentFloor == transferFloor + 1) {
        lock.notifyAll();
    }
}
  • 两个if都要在锁中,保证一个轿厢移动完,另一个轿厢才开始判断,先判断再阻塞没有意义
  • 一定将currentFloor++;放在锁内,保证释放锁的时候,楼层已修改,否则另一轿厢得到的信息会延迟

bug分析

三次作业只有第二次强测出现了bug,未RECEIVE就移动,可以确定是线程安全问题引发的WA,但经多次检查,并没有发现哪里不安全,但定位到了具体出问题的代码,于是加了一个特判,补上了RECEIVE,后续也没有新的bug,但还是没从根源解决问题,一直很苦恼。

  • debug方法
    • 如果是WA,就比较好debug,根据代码逻辑到对应位置检查就好了(除了线程安全bug)
    • 如果是CTLE,基本可以确定wait()失效,导致轮询
    • 如果是RTLE,就是死锁,或者wait()无法被唤醒

心得体会

最大的体会就是,理解和掌握线程安全和各种锁的使用非常重要。虽然没有在调度器以及调度策略上费工夫(卷性能),但在设计各种锁,维护我的自由竞争的一大坨线程安全问题的时候,也绞尽脑汁,在这个过程中,我对锁的理解有了进一步的提升。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值