BUAA_OO_2020_Unit2_Summary

Posted palemodel

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了BUAA_OO_2020_Unit2_Summary相关的知识,希望对你有一定的参考价值。

BUAA_OO_2020_Unit2_Summary

简述

通过Unit2的学习,我了解到Java多线程的相关知识,认识到单例模式、生产者-消费者模式、观察者模式、工人模式等设计模式,并通过设计基于SSTF算法的电梯加深对多线程知识的理解,同时将一部分设计模式加以应用。本博文从设计策略概述、架构可扩展性分析、程序结构分析、Bug分析(含自身Bug分析与他人Bug分析)及心得体会五方面展开,总结在Unit2中的收获。

设计策略概述

第一次作业

    • 主类(MainClass)
    • 输入处理类(InputHandler, 线程形式)
    • 管理者类(Manager)
    • 电梯类(Elevator, 线程形式)
  • 设计模式

    • 使用生产者-消费者模式,输入处理类充当”生产者“,管理者类充当”托盘“,电梯类充当”消费者“,请求充当”产品“。

    • 使用单例模式

      • 构造单例的传统方法——饿汉法

        private static Manager instance = new Manager();
        
        private Manager() {
        
        }
        
        ;
        
        public static Manager getInstance() {
            return instance;
        }
        
      • 本次作业中使用的构造单例的方法——双重锁法

        private static volatile Manager instance;
        
        private Manager() {
        
        }
        
        ;
        
        public static Manager getInstance() {
            if (instance == null) {
                synchronized (Manager.class) {
                    if (instance == null) {
                        instance = new Manager();
                    }
                }
            }
            return instance;
        }
        
      • 两种单例构造方法的评价

        • 饿汉法:线程安全,但instance在Manager类加载时即实例化,导致内存垃圾的产生。
        • 双重锁法:线程安全,克服饿汉法产生内存垃圾的缺点,需使用volatile关键字修饰instance变量以消除重排序所致的风险。
  • 基本思路

    • 关键类说明

      • 电梯类
        • 属性:①物理性质:shiftTime、openAndCloseTime、state、curFloor,② 控制性质:mainRequest、mainRequestType,③管理者单例。
        • 方法:①动作方法:pickMainRequest、finishMainRequest等,②播报方法(制造stdout的方法):showPersonIn、showPersonOut等。
        • 责任:专注于自身动作的完成与播报。
      • 管理者类
        • 属性:①外部请求队列,②内部请求队列,③结束控制符,④管理者单例。
        • 方法:①外部队列相关方法:addOutRequest、isOutRequestsEmpty,②进出方法:getIn、getOut,③主请求分配方法:waitForMainRequest、lookForMainRequest,④时间预测检查方法:checkLookFloor。
        • 责任:专注于请求的管理与分配。
    • 调度策略:以主请求mainRequest为电梯的控制主线,电梯的工作始终围绕一个目标——完成当前主请求。在电梯完成一主请求后,若管理者暂不可为其分配新的主请求,则电梯陷入wait状态。当管理者接收到输入端传来的请求时,将请求加入外部请求队列,并notifyAll,唤醒处于wait状态的电梯向管理者申请新的主请求。

    • 捎带策略:当且仅当当前楼层存在目标方向与电梯运行方向相同的外部请求时,管理者让电梯进行捎带。每次捎带让当前楼层的所有外部请求均成为电梯内部请求。

    • 优化策略:采用一种紧扣局部优解的贪心算法——SSTF算法分配主请求,并辅以时间预测提升性能,具体说明如下:

      • SSTF算法流程示意图:

        技术图片

      • 时间预测的例子:当前电梯要将主请求从10层运至1层,当电梯从5层到达4层时,判断6层、7层(即时间预测的范围为two floors)是否有向下的请求。若有,在电梯本应播报“到达4层”并将当前层号置为4时,让电梯”反抗“主请求的操纵,并以”光速“到达6层(即改为播报”到达6层“并将当前层号置为6)。PS:时间预测仅改变电梯一时的运行方向,电梯的主要运行方向仍由主请求操纵。此方法融合了一定的LOOK算法回头的思想,虽“量子电梯”有悖现实,但经过大量随机数据测试,可以有效提升SSTF算法在作业性能上的缺陷。

    • 结束策略:当输入处理器读入的请求为null时,将管理者的结束控制符toEnd由false置为true,并唤醒处于wait状态的电梯。此时被唤醒的电梯继续寻找新的主请求,若寻找不到,直接结束线程;否则,处理找到的新的主请求,直到寻找不到主请求为止。

    • 线程协同策略:本次作业中,将输入处理类与电梯类(即生产者与消费者类)作为线程,管理者仅作为生产者与消费者进行数据交互的非线程类。输入处理类与电梯类的共享变量仅有两个,一是外部请求队列outRequests,另一是结束控制符toEnd。考虑到管理者类中关于这两个变量的方法均不长,再加上完成本次作业时对object锁的理解还不太深入,故采用对这些方法进行synchronized加锁的方式确保线程安全。

第二次作业

  • 新增类

    • PersonRequest1类(继承自课程组提供的PersonRequest类)
  • 设计模式

    • 沿用第一次作业中的生产者-消费者模式和双重锁单例模式。
  • 基本思路

    • 关键类说明

      • PersonRequest1类(新增类)
        • PersonRequest类站在实际的角度看楼层,即楼层集合为([-3, -1] ∪ [1,16]) ∩ Z;而PersonRequest1类站在处理的角度看楼层,即楼层集合为[-2, 16] ∩ Z,实际的-3、-2、-1层被映射到-2、-1、0层,从而保证层号的连续性,便于处理。
        • 输入处理器将所有输入的PersonRequest对象转为PersonRequest1对象传给管理者;在各电梯与管理者中,只关注PersonRequest1对象,但要注意输出时层号的转化。
        • 属性:isOutMainRequest,标明该请求是否同时满足以下两个条件:①为一个电梯的主请求;②该电梯是从外部请求队列中寻得该请求作为主请求的。
        • 方法:①人员获取方法:继承自PersonRequest类的getPersonId,②相关楼层获取方法:重写的getFromFloor与getToFloor,③属性isOutMainRequest相关方法:becomeOutMainRequest与isOutMainRequest,④重置fromFloor的对象返回方法:setFromFloor。
      • 电梯类与管理者类(原有类)
        • 由于各电梯的内部请求队列(直观理解为电梯内的人)不尽相同,将内部请求队列及其相关方法放入管理者类中,显然不合适,故将这部分内容调整至电梯类中。此时,电梯可以看作是其物理存在与内部人员构成的整体。
        • 经过这一调整后,电梯类和管理者类的责任可总结为:
          • 电梯:自身动作的完成与播报、内部请求队列(内部人员情况)的更新
          • 管理者:外部请求的管理与分配、闲置电梯的调度选择
    • 调度策略:仍沿用第一次作业中的思路,将mainRequest作为电梯控制的主线。所不同的是,在引入多部电梯后,管理者类需要为每个新加入的外部请求,安排合适的闲置电梯。闲置电梯的安排仍然采用贪心的思想,即为外部请求安排最近的闲置电梯。

    • 捎带策略:电梯到达每一层时,先让到达目的地的所有乘客出电梯,后执行类似于下述的伪代码:

      if (主请求在当前层进入) {
          要捎带;
          if (电梯内满人) {
              用贪心思想选取电梯内一人暂时离开电梯,并投出新的外部请求;
          }
          主请求先进入电梯;
          让外部请求按一定优先顺序进入电梯,直到电梯内满人;
      } else {
          if (电梯内满人) {
              不捎带;
          } else {
              if (有同向请求) {
              	让外部请求按一定优先顺序进入电梯,最多到电梯内满人;    
              } else {
                  不捎带;
              }
          }
      }
      return;
      
    • 优化策略:保留第一次作业中的SSTF算法和时间预测。

    • 结束策略:与第一次作业相类似,略有不同的地方在于toEnd置为true后,管理者类要将所有的处于wait状态的电梯逐一唤醒。

    • 线程协同策略:本次作业中,仍然要关注toEnd与outRequests这两个生产者、消费者所共享的变量。与第一次作业不同的是,为了更灵活地使用锁,更方便地完整特定线程的唤醒,并更好地监控锁的重入深度以方便调试,我将所有的synchronized方法锁全部用ReentrantLock锁对象lock进行处理。

      • 特定线程唤醒的方法:在管理者类中设一conditions队列,与elevators队列中的每一个电梯相对应,充当每一个电梯的“唤醒师”。

        要让特定电梯睡眠时,采用语句:

        conditions.get(elevator.getEleId() - ‘A‘).await();
        

        要唤醒特定电梯时,采用语句:

        conditions.get(elevator.getEleId() - ‘A‘).signal();
        
      • 监控锁的重入深度的方法:使用ReentrantLock对象的getHoldCount方法。

第三次作业

  • 新增类

    • 安全输出类(SafeOutput)
  • 设计模式

    • 沿用前两次作业中的生产者-消费者模式和双重锁单例模式。
  • 基本思路

    • 关键类说明

      • 安全输出类(新增类)
        • 设置加锁的println静态方法,起到确保输出安全的作用。
        • 事实上课程组提供的TimableOutput类的println方法已有synchronized锁的保护,故SafeOutput类按理可以省去,但出于心理作用还是加上了
      • PersonRequest1类
        • 将映射后的fromFloor与toFloor作为自身属性保存,新增属性nextRequest,表示该请求是否包含换乘后请求。
        • 对以下类型的请求进行静态换乘处理:
          • 起点与终点中的一者不在1 ~ 15层的范围内,另一者在该范围内
          • 起点与终点均在1 ~ 15层的范围内,其中的一者为3,另一者为偶数
        • 将1层、5层、15层作为请求拆分点。
        • 请求拆分的例子:
          • PersonRequest对象的起止情况:-2 ~ 10(中间不含第0层)
          • 若不拆分,PersonRequest1对象的起止情况:-1 ~ 10(中间含第0层)
          • 若拆分,PersonRequest1对象的起止情况:-1 ~ 1,其nextRequest属性对象的起止情况:1 ~ 10
    • 调度策略:仍沿用第二次作业中的思路,所不同的是电梯与外部请求的距离衡量公式发生变化,变为Math.abs(楼层差) * 电梯移动一层的时间。

    • 捎带策略:电梯到达每一层时,先让到达目的地的所有乘客出电梯,后执行类似于下述的伪代码:

      if (主请求在当前层) {
          要捎带;
          if (电梯内满人) {
              用贪心思想选取电梯内一人暂时离开电梯,并投出新的外部请求;
          }
          主请求先进入电梯;
          让可完成的外部请求按一定优先顺序进入电梯,直到电梯内满人;
      } else {
          if (电梯内满人) {
              不捎带;
          } else {
              if (有可完成的同向请求) {
                  if (电梯类型非A) {
                  	让可完成的外部请求按一定优先顺序进入电梯,最多到电梯内满人;    
                  } else {
                      让可完成的同向外部请求按一定优先顺序进入电梯,最多到电梯内满人;
                  } 
              } else {
                  不捎带;
              }
          }
      }
      return;
      
    • 优化策略:保留前两次作业中的SSTF算法和时间预测,并进行如下调整:

      • SSTF算法:对于B类型、C类型电梯不作更改。对于A类型电梯,为防止出现“饿死现象”,当仅有一架此类型电梯的时候,若电梯内部无请求且另一端(将A可停靠的底部楼层作为一端,上部楼层作为另一端)有请求,则直接将另一端的最远请求作为主请求。当有两架以上此类型电梯的时候,若有两电梯分散在两端,则遵循原有的SSTF算法,尽可能让两电梯保持在两端;否则,按照有一架电梯时的改良版SSTF算法,尽可能将一架电梯分派到另一端。
      • 时间预测:时间预测的范围改至3层,并且3层时间预测只对电梯内人数小于4情况有效。
    • 结束策略

      if (输入处理器读入请求null || 一架电梯在某一层完成一次进出) {
          输入处理器将管理者的结束控制符toEnd由false置为true;
          唤醒所有处于wait状态的电梯;
          被唤醒的电梯继续寻找新的主请求;
          if (寻找不到新的主请求) {
              if (Condition1:所有电梯的内部请求nextRequest && Condition2:外部请求队列为空) {
                  直接结束电梯线程;
              } else {
                  电梯陷入wait状态;
              }
          } else {
              处理找到的新的主请求,直到寻找不到主请求且满足Condition1与Condition2为止;
          }
      }
      
    • 线程协同策略:本次作业沿用第二次作业中的lock线程同步机制以及condition唤醒特定线程的方法,所不同的是取消管理者的conditions队列,而在每个电梯中增加一Condition对象作为属性,相当于将电梯的“唤醒师”封装至电梯中。

    • 一种优化策略:基于“流水线”的思想,设一合适的时间阈值,一个包含换乘后请求的请求加入外部请求队列时,估计MAX(换成前请求的执行时间,MIN(闲置电梯到达换乘后请求出发楼层的时间)),若该值小于所设定的阈值,则表明适合提前安排换乘后请求为一闲置电梯的新的主请求。若所安排的电梯到达换乘后请求的出发楼层后,换乘前请求还未完成,则wait等待。多次随机模拟表明,这种做法能在一定程度上提升性能(当然阈值要设好),但由于本人在设计中出现了死锁,最终难以复现,又没有时间Debug了,最后只好铩羽而归。

    • 评测中的灵异事件:本人在强测中,一个点99.8+,部分点99.99+,部分点100,唯独一个点出现了令人瞩目的80(qaq qaq qaq)。在本机跑过多次后,时间均为85s,而评测时显示的时间为95s。通过分析强测的stdout,我发现了如下的奇异输出

      [77.3810]ARRIVE-6-B
      [77.3820]OPEN-6-B
      [81.0910]IN-286-6-B
      …… ……
      [82.9920]ARRIVE-9-B
      [82.9940]CLOSE-8-X1
      [85.3750]ARRIVE-10-B
      

      (99.8+的那个点也有出现中间停了4s的情况)

      经过与wsb大佬的讨论,这是由于强测的评测环境高度并发,设有各种各样的系统时间切片所致的。不过课程组明确表示,所有请求的投放时间都是一致的,会保证评测的公平性。

架构可扩展性分析

  • 架构总结:第三次作业中的管理者,既充当生产者消费者模式中的“托盘”,即外部请求队列放置的地方,又充当所谓的调度器,即将请求分配给合适的电梯。所有的分配均围绕主请求mainRequest进行,既包括输入处理器向外部请求队列加入新请求时,将新请求分配给合适的闲置电梯并作为其主请求,又包括电梯完成主请求后,按SSTF算法为电梯指定合适的新的主请求。同时,管理者亦可对时间预测的结果进行判断,即决定电梯是否要掉头“反悔”。
  • 可扩展点总结:管理者中的调度相关方法,如addOutRequest、lookForMainRequest,是本次作业细节调整的重要扩展点。相应的,时间预测的范围亦可根据电梯传给管理者的checkRange参数进行调整。但由于SSTF算法下电梯的工作与主请求关系过紧,基本大框架不易变更,但可以根据架构作调整。例如在第三次作业中,我本想将A类型电梯的调度算法改为LOOK,但受程序框架的限制,发现彻底的调整的代价较大,故直接在SSTF算法的基础上进行调整,亦可达到类似的效果。
  • SRP原则:本原则意为“每个类或方法都只有一个明确的职责”。从下一部分“程序结构分析”中可以看出,本次设计迭代到第三次,Elevator类与Manage类已经臃肿不堪,显然是十分不良的设计。之所以会设计成这样,很重要的一个原因是在做第一次作业的设计时,考虑到Elevator类和Manager类的方法都不算太多,所以将许多职责塞给了二者。而随着开发迭代的过程中代码量的不断上升,这两个类内的新方法不断加入,原有方法细节不断地完善,造成类所承受的负担越来越重。因此,我根据之前总结的类的职能,将第三次作业中的这两个类做如下的拆分调整,让设计更加符合SRP原则的要求:
    • 新增一播报器类,将与产生stdout信息的方法,例如showPersonIn等、showPersonOut、showArrive、showOpenDoor、showCloseDoor等封装至该类中,并单例化后作为电梯类及管理者类内部的一个属性。虽然在本次作业中,stdout信息较少,不采用这一设计方法也没啥问题,但实际迭代开发过程中如果有更多的需要播报的stdout信息,单独抽出来管理显然会比较舒服。
    • 新增一内部请求队列类,将与内部请求队列本身相关的方法,例如isInRequestsEmpty、addInRequest、getFirstFinishInRequest(配合SSTF算法)、containNextRequest等封装至该类中,并实例化一对象作为电梯类内部的一个属性。
    • 新增一电梯队列类,将与电梯的加入、撤销相关的方法,例如addElevator等封装至该类中(注意锁的保护),并单例化后作为管理者类及唤醒器类(后续介绍)内部的一个属性。虽然本次作业中,该类仅需设置一个方法addElevator,但考虑到后期,可能会有电梯的撤销,甚至是电梯的检修(在该类中分出正常电梯和检修电梯两类电梯队列),所以这种设计对提升程序扩展体验是有意义的。
    • 新增一外部请求队列类,将与外部请求队列本身相关的方法,例如isOutRequestsEmpty、addOutReqeust(此时变为单纯的outRequests.add(request),不融入闲置电梯唤醒的部分)、getFirstPickOutRequest、getOtherEndsOutRequest、checkLookFloor等封装至该类中(注意锁的保护),并单例化后作为管理者类及分配器类(后续介绍)内部的一个属性。
    • 新增一唤醒器类,将原来管理者类addOutRequest方法中的闲置电梯选取部分封装至该类中,并单例化后作为管理者类内部的一个属性,让新加入的外部请求去选择成为哪个闲置电梯的主请求
    • 新增一分配器类,将原来管理者类的主请求分配方法,例如lookForMainRequest、outLookforMainRequest等封装至该类中(注意锁的保护),并实例化一对象(可单例化)作为管理者类内部的一个属性,为刚完成主请求的电梯分配新的主请求
    • 经过上述处理后,电梯类更加专注于自身动作的完成,对于内部人员信息的更新仅保留getOut、getInAndOut这两个宏观方法,将具体细节的完成交给内外部请求队列类。而管理者类相当于唤醒器类与分配器类的“总师”,起到总指挥的作用,安排唤醒器与分配器在合适的时机完成工作任务,对于外部请求队列的维护仅保留getIn这一宏观方法,将具体细节的完成交给外部请求队列类。
  • OCP原则:本原则意为“无需修改已有实现,而是通过扩展来增加新功能”。本单元作业的迭代开发中,除第二次作业进行了一些方法位置的调整,均未对前一次作业的实现进行大规模的改动,而是通过增加新的方法、属性或修改原有方法中的细节,来实现新功能。从这个层面上看,OCP原则遵循的较好。
  • LSP原则:本原则意为“任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误”。第三次作业的设计中PersonRequest1类继承自PersonRequest类,电梯与管理者所看到的所有人的请求均为PersonRequest1类对象。在输入处理类中出现的所有PersonRequest类对象输入,均被替换为子类PersonRequest对象,并且在子类中重写getFromFloor与getToFloor这两个重要方法。
  • ISP原则:本原则意为“通过接口来建立行为抽象层次具有更好的灵活性”。第三次作业中,由于仅对A类型电梯采用不同类型的主任务分配算法,分配方式在管理者类的相应方法中通过if-else语句即加以区别了。但如果未来要加入更多类型的电梯,不同类型的电梯有不同的主任务分配方法,甚至同一电梯在不同场景下的分配方法都不同,那么我认为完全可以利用ISP原则可以做如下处理:
    • 建立多个分配器类,统一实现分配接口的方法。
    • 建立分配器工厂,根据传入的电梯型号及状态返回合适的分配器对象。若特定型号的电梯在运行过程中分配方法始终不变,则可将该分配器对象作为电梯本身的属性,在电梯的构造函数中生成。
    • 相较于电梯而言,分配器处于工作量较小的状态,仅在电梯闲置时发挥作用。因此,可以考虑将同一类的分配器做成单例,让一个分配器管理相匹配的所有电梯,即分配器与电梯是“一对多”的形式。这样既可减少分配器对象的数量,又能更加充分地利用每一分配器。
    • 对于唤醒器亦可有类似的处理。
  • DIP原则:即依赖倒置原则。由于本单元作业的变化较小,故此原则的遵循情况对迭代开发的影响不大。但如果在未来开发中要不断变动电梯的唤醒或分配方法,则引入接口让多个类去实现是一个不错的主意,具体方案见ISP原则的分析部分。由此可见,ISP原则与DIP原则存在一定的相通之处。

程序结构分析

第一次作业

  • 度量结果
    技术图片
    技术图片

  • 类图
    技术图片

  • 协作图
    技术图片

  • 评价

    • 类图十分简明扼要,体现出生产者消费者模式的特点,即生产者(InputHandler)与消费者(Elevator)共用一个托盘(Manager)
    • 方法复杂度总体相当合适。与时间预测相关的方法Elevator.moveTo与Manager.checkLookFloor由于分支较多而出现较大的iv(G)或ev(G),但仍在可以接受的范围内。
    • Elevator类与Manager类略显臃肿。

第二次作业

  • 度量结果
    技术图片
    技术图片

  • 类图
    技术图片

  • 协作图
    技术图片

  • 评价

    • 本次作业的类图仍然较为简明,比较明显的一个特点是Manager类与Elevator类之间相互存在一对多的关系,耦合较为紧密,可以通过将电梯队列单独封装出来,降低这两类之间的耦合。
    • 方法复杂度总体较为合适。除Elevator.moveTo方法外,与进出相关的方法Elevator.getInAndOut与Manager.getIn同样出现较大的iv(G)或ev(G),而Elevator.getInAndOut是Manager.getIn的调用者,受其牵连。可以考虑将Manager.getIn方法中的捎带有效性判断部分用Manager类的私有方法提取出来。
    • Elevator类与Manager类的方法进一步增多,臃肿程度提高。

第三次作业

  • 度量结果
    技术图片
    技术图片

  • 类图
    技术图片

  • 协作图
    技术图片

  • 评价

    • 本次作业的类图同第二次作业相比,几乎没有类之间关系的变化,由此可见在迭代开发的过程中整体架构的维持还是比较稳固的,OCP原则遵循的较好。
    • 方法复杂度总体较为合适。注意到这次新增了两个复杂度较高的方法,一个是Manager.addOutRequest方法,另一个是Manager.getOtherEndsOutRequest方法。后者要判断的条件较多,复杂度不必刻意降低;前者可通过将唤醒器单独出来并设置方法,也就是将唤醒功能封装出来,实现复杂度的降低。
    • Elevator类与Manager类的方法继续增多,已臃肿不堪,既降低编程体验感,又不利于实现更多功能的扩展。相关的解决方案见架构可扩展性部分中对SRP原则的解读。

Bug分析(含自身Bug分析与他人Bug分析)

第一次作业

  • 在互测、强测中,均未出现本人自身的bug;中测第一次提交遍地红WA,原因是把人从电梯中出去的信息播报打成“sb-OUT-x”了

  • 本次作业在私下测试的过程中,可以说是相当顺利的,除了一些极其容易测出来的“打错型”Bug外,并未发现任何Bug。尤其是在线程协同方面,可以说是没能吃到苦头。

  • 在互测中,用同一样例成功hack到两名同学

    • stdin:

      [5.4]1-FROM-1-TO-7
      [9.4]2-FROM-7-TO-10
      [9.4]3-FROM-7-TO-6
      
      • 第一位同学的错误:死锁错误——调度器类的锁被输入进程霸占,电梯类获取不到调度器类的锁,无法正常执行。
      • 第二位同学的错误:功能性错误——同时收到两个反向请求的时候,电梯根据其中一个请求控制运行方向,而另一请求被直接废弃,得不到后续的响应。
      • hack策略:构造手动样例,测试程序是否可协调同一时间投放的两个方向相反的请求。

第二次作业

  • 在互测、中测、强测中,均未出现本人自身的bug。

  • 本次作业在课下开启自动评测机后,发现了多处Bug,其中大部分是“打错型”Bug,在此只能说一个自创词——熬夜伤目。有一个Bug让我印象颇深,至今还没有弄清楚其中的原因,我在此作下介绍:

    • 我原来采用的条件唤醒方法是,不设Condition对象,而采用synchronized(电梯对象)的方式,让电梯陷入wait和被唤醒均在此synchronized区块中。按理来说各电梯对象不尽相同,wait对应的monitor自然不尽相同,这和为各电梯对象分配不尽相同的Condition对象是类似的。但实测中极少部分样例出现类似死锁的现象。经过println大法以及getHoldCount方法发现,两架并不处于wait状态的电梯在执行电梯类内部代码时,要进入管理者类中执行两个不同的lock(管理者类的属性锁)区域内的代码,此时lock的holdCount为0,按理来说会有一架电梯先获得管理者类的属性锁,获得管理者单例这个监控器,执行相应区块代码,然而两架电梯均发生了阻塞现象。但此时的holdCount明明为0,而且换成类似的Condition大法后就没有这样的问题的了,所以个人表示一脸懵逼......

      若有同学遇到过类似的Bug,即对象锁特定唤醒不管用,而一一对应的Condition特定唤醒管用的情况,或是对此Bug的原因有个人见解,欢迎添加VX号taikan199私聊我!!!

  • 在互测中,成功hack到一名同学

    • stdin:

      [0.0]3
      [2.3]1-FROM-7-TO--2
      [2.3]2-FROM-7-TO-9
      [2.3]3-FROM-7-TO-10
      [2.3]4-FROM-7-TO-3
      [2.3]5-FROM-7-TO-6
      [2.3]6-FROM-7-TO--1
      [2.3]7-FROM-7-TO-4
      [2.3]8-FROM-7-TO-2
      [2.3]9-FROM-7-TO-1
      [2.3]10-FROM-7-TO--3
      [2.3]11-FROM-7-TO-5
      [3.5]12-FROM-7-TO-8
      [3.5]13-FROM-9-TO--3
      [3.5]14-FROM-9-TO-10
      [3.5]15-FROM-9-TO-3
      [3.5]16-FROM-9-TO-7
      [3.5]17-FROM-9-TO--1
      [3.5]18-FROM-9-TO-1
      [3.5]19-FROM-9-TO-5
      [3.5]20-FROM-9-TO-8
      [3.5]21-FROM-9-TO-4
      [3.5]22-FROM-9-TO-6
      [4.7]23-FROM-9-TO--2
      [4.7]24-FROM-9-TO-2
      [4.7]25-FROM-7-TO-1
      [4.7]26-FROM-7-TO-8
      [4.7]27-FROM-7-TO-4
      [4.7]28-FROM-7-TO-2
      [4.7]29-FROM-7-TO--2
      [4.7]30-FROM-7-TO-3
      [4.7]31-FROM-7-TO--3
      [4.7]32-FROM-7-TO-5
      [4.7]33-FROM-7-TO-10
      [5.9]34-FROM-7-TO-9
      [5.9]35-FROM-7-TO--1
      [5.9]36-FROM-7-TO-6
      [5.9]37-FROM-10-TO--1
      [5.9]38-FROM-10-TO--3
      [5.9]39-FROM-10-TO-4
      [5.9]40-FROM-10-TO-5
      [5.9]41-FROM-10-TO-7
      [5.9]42-FROM-10-TO-2
      [5.9]43-FROM-10-TO-8
      [5.9]44-FROM-10-TO-9
      
      • 同学的错误:线程安全错误——一处地方对共享队列未进行加锁保护,引发同步问题。
      • hack策略:构造手动样例,测试程序是否可处理好同一时间投放的出发点在同一楼层的请求。

第三次作业

  • 在互测、中测、强测中,均未出现本人自身的bug。

  • 本次作业在课下共发现两个主要Bug,下面展开介绍

    • 对于一些包含换乘后请求的请求,比如从5楼到15楼,再从15楼到20楼,在换乘点,即15楼,会先报IN-sb-15-A,后报OUT-sb-15-C
      • 原因:在前两次作业中,我是先生成进、出者队列,再分别根据两个队列进行信息播报。而在本次涉及换乘的作业中,以上述例子为例,15层的C类型电梯在进出阶段得到一个包含sb的出队列,而此时同一层的A类型电梯收到sb后得到一个包含sb的进队列,但两个电梯的信息播报是可以并行的,可能A类型电梯先利用其进队列播报IN-sb-15-A后,C类型电梯才开始利用其出队列播报OUT-sb-15-C。
      • 解决方案:每次从inRequests队列中出一请求之前,先播报OUT信息,保证OUT信息播报在请求经过转乘进入新的电梯之前,进而保证其在相对应的IN信息播报之前。
    • 在部分情况下,一些换乘后请求得不到正常实现
      • 原因:根据前两次作业的结束策略,当管理者的结束标志符toEnd被置为true,且A类型电梯无内部请求时,即可结束线程。而此时,可能有一架C类型电梯,准备从当前的10层上升至15层,放出一目的地在18层的人,让A类型电梯带着这个人从15层上升至18层。而此时,如果所有的A电梯提前结束,这个人到达18层的愿望就无法实现了。
      • 解决方案:改变结束策略,新增结束条件所有电梯的内部请求nextRequest均为null以及外部请求队列为空,新增唤醒所有闲置电梯的时机一架电梯在某一层完成一次进出
      • 解决方案的提升:按照荣老师的说法,这种结束策略需要让一架电梯了解所有电梯的内部请求队列情况,电梯与电梯之间交互过紧,不是一种很好的做法。因此我觉得可以在管理者类中维护一初始值为0的计数器,每当有包含换乘后请求的请求进入电梯时,计数器值+1;每当换乘后请求进入电梯时,计数器值-1。根据计数器值是否为0即可判断第一个新增结束条件是否成立。
  • 在互测中,成功hack到一名同学

    • stdin:

      [1.0]X1-ADD-ELEVATOR-C
      [1.0]182-FROM--1-TO-7
      [1.0]X2-ADD-ELEVATOR-B
      
      • 同学的错误:异常错误——无任何stdout,直接爆出Exception in thread "Thread-1" java.lang.ArrayIndexOutOfBoundsException: 4(即数组越界)。
      • hack策略:自动测试发现错误,并截取其中使错误产生的部分作为hack样例。
  • 本单元的3次hack,均采用手动测边界、自动测功能的方法,这与在中测阶段测试自身程序的方法相同。

  • 本单元的4个成功hack,均由本人独享,其中第一次作业和第二次作业的成功hack结果还是在最后一次cd之后才出现的,颇为惊险刺激。事实上,第二次与第三次作业中,本人的room中各有一位同学出现死锁,但触发频率极低,所以最后都没hack成。(尤其第三次作业,room中多位同学疯狂开火炮,结果全部naive)

  • 同第一单元的测试相比,本单元测试有以下差异:

    • 功能错误相对较少,关注点更多在线程问题上。第一单元作业更加看重程序的鲁棒性,从解析环节到运算环节再到化简环节,过程相对复杂,第三次作业中更是出现易出错的树形递归结构。本单元作业过程相对简单,真正意义上的“边界”情况较少,更多的错误集中在由于锁的使用不当,线程之间协调不当而导致的线程问题上。当然,对功能性问题亦要有足够的重视。例如对第一次作业,关注对算法不利的样例是否会造成TLE;第二次作业,关注让位主请求是否会出现错误,第三次作业,关注换乘是否会引发冲突。同时,针对Java语法机制的良好编程习惯亦要时刻遵守,本人在第三次作业中的成功hack正说明这一点,慎用数组,慎用数组,慎用数组
    • 自动化测试的重要性更加凸显。许多死锁问题,触发的频率很低,如果不通过大量随机样例加以测试,很难在短时间内发现这些问题。有些同学虽既未在公测中出错又未被hack,但这并不代表程序不存在问题,很可能互测room内的同学发现了他们的错误,准备开枪又无力命中。当然,运气也是实力的一部分,不过从一些场景来看,任何一个微小的错误,即使触发频率很低,也不代表不存在导致严重结果的可能,所以一名合格的programmer是不能在任何一个细节上马虎大意的。

心得体会

  • 线程安全方面
    • 锁的使用是艺术,不多不少记心中。既要保证多个线程对共享数据的同步访问不出问题,又要保证死锁不会产生。
    • 自动化测试必要凸显。对于死锁问题的调试,既可利用idea自带的照相机,又可利用windows命令行,还可利用一些现成的GUI工具。但大工程中的死锁往往呈现出一种特点,即复现困难,因此自动化测试在死锁问题的测试上是相当有必要的,让小概率事件在大样本的逼迫下现形。
    • 用好ReentranLock对象和Condition对象。ReentranLock对象与synchronized关键字基本等效,但使用时更显灵活性;而Condition对象的使用则对特定唤醒大有裨益。借助于ReentranLock对象,还可监控锁的重数,查看是否有对应Condition对象下的等待者,有助于初学者更好的理解锁的重入、线程状态转变等问题。因此,建议课程组将ReentranLock对象的使用提前到synchronized关键字之前教学,这样可以让刚接触多线程的同学们对知识点形成更加直观的理解。
  • 设计原则方面
    • SOLID原则是自我审视的一大利器。本人通过撰写博客作业,对照SOLID原则,想出了许多改良设计的方案。虽然SOLID原则仅仅是提纲挈领的设计原则,并未像设计模式一样涉及具体的设计方法,但在设计前,将自己的方案与SOLID原则进行对照是很有必要的,这样才能更准确地朝优质代码的方向前进。
    • 一开始就将类适度切细,别存侥幸心理。本人一开始觉得电梯作业的问题并不复杂,未将电梯与管理者中职能切细,而是为电梯和管理者施压。一开始这种压力可以说是轻压,但随着迭代开发过程中细节的不断加入,这两个类不知不觉就变得异常臃肿。在后期微调的过程中,虽然有预留调整的切入口,但一个类中过大的代码量让人眼花缭乱,编程体验感不佳。更重要的是在以后的实际工作中,我们难免会面临比这更加复杂,功能上升更加迅猛的迭代开发项目,因此严格遵从SRP原则,让每一个类职能尽可能单一,是很有必要的。
    • 与其重构,不如多考虑一下如何在原有架构的基础上博采众长。由于每次迭代均未在架构上大开倒车,本人三次作业第一版本的完成时间可以说是逐一递减的。事实上,在完成第一次作业时,发现SSTF算法在某些数据点表现不佳,后结合LOOK算法掉头的思想,加了二层时间预测,性能上取得的肉眼可见的进步。而第三次作业的A类型电梯的处理,由于纯SSTF并不很适用,我在原有的基础上进行魔改,保证紧扣主请求这一原有框架不被破坏。总之,在决定重构前,不如多问问自己,让自己想重构的迭代上升点是什么?这个迭代上升点会破坏原有架构的哪个环节?是否有办法为这个破坏点整容以适应新的需求,以更好地满足OCP原则?

以上是关于BUAA_OO_2020_Unit2_Summary的主要内容,如果未能解决你的问题,请参考以下文章

BUAA_OO_2020_Unit3 Summary

BUAA_OO_2020_Unit3_Summary

BUAA_OO_2020_Unit3_Overview

BUAA_OO_2021_Unit3_Summary

BUAA_OO_第三单元作业总结

BUAA_OO_第四单元