OO第二单元总结

OO第二单元作业

前言

本次OO作业,我投入了大量精力,包括多线程知识的了解,评测机的设计与迭代,以及数据生成器的设计,虽然最后卒于一个奇怪的且无法复现的关门问题,但是总体来说还是学到了不少东西,第五次次作业的游刃有余到第六次作业的一个调度的小问题,第七次作业卒于信号的维护少加了一把锁,就如同Red Dead中的西部牛仔从黄金年代的如日中天到转型年代的分崩离析,最后由于工业化的浪潮,随着Arthur Morgan倒在夕阳下的斜坡,John Marston被警探的乱枪打死,西部的传说最终落下帷幕,这个波澜壮阔却略有遗憾的故事。

第五次作业

第一单元由于没有提前思考好架构的问题,摆了大烂,所以我对OO的作业需要提前设计出一个良好的架构深以为然,因此在电梯单元的开始,我就做好了花大量时间设计架构的准备。从开始看黑书全面了解关于线程的预备知识,再翻阅了大量之前学长的博客,了解他们的设计思路,做了很多的预备工作。

同步块的设置和锁的选择

在咨询了学长之后,我了解到,要保证线程安全,一个很重要的点就是在设计的时候充分考虑,尽量操作共享数据的方式,将操作集中在一两个方法。而本次作业,生产者是读入的请求,消费者是电梯。中间其实只有一个点是两方需要接触的,这个需要接触的点可以抽象为一个请求队列,读入器读入请求放入队列中,电梯从队列中取出请求进行调度,所以实际上的共享数据就只有一个队列,而这个队列在java库中已经封装好了,就是BlockingQueue,如果对于java的线程安全足够的理解可以直接使用。该接口的使用十分方便,只需要使用两个方法,一个是put(request)将请求放入队列之中,一个是take()从队列中取出请求,这两个方法都由java封装是线程安全的,take()方法会在队列为空的时候将线程挂起,在put()之后重新换起线程,可以很好的达到wait-notify的效果,实现线程之间的协作。RequestQueue在继承之后可以直接作为容器使用。

  public class RequestQueue extends LinkedBlockingQueue<PersonRequest> { }

整体的架构图如上,最终只有一个需求队列需要共享数据,所以我没有其他地方加锁。

调度器设置和调度策略

本次作业由于只有一种电梯,所以不需要调度器分配请求,所以没有设置调度器,,直接使用look算法,在电梯内部实现了行为控制降低各个类之间的耦合度,也降低了出现线程安全的风险。

  • 电梯在上下来回扫描,期间能捎带就捎带。
  • 在同方向上没有请求之后电梯转向。

整体架构思路

设计模式

本次作业的对象其实非常明了,可以采用生产者-消费者模式,将电梯请求看作生产者,电梯当作消费者,中间只需要一个接触的托盘,就可以完成本次任务。

主要类介绍

  • RequestQueue类为请求队列,唯一的共享数据。

  • RequestReader用以读取数据的线程。

  • Elevator电梯线程,run方法抽象电梯运行的过程。

    电梯的运行逻辑在内部完成,本质上是一个状态机,有开门关门移动等状态,每次到达一个新楼层先判断,是否需要上下乘客,然后用内部的judge方法判断是否开关门,当电梯内部没有乘客且侯乘表当中没有侯乘的乘客时将电梯线程挂起,避免轮询。

  • WaitTable各层楼之间的侯乘表。

    内部维护了各楼层之间的侯乘乘客,look策略的一个关键的点就是要判断同方向上有无侯乘表,这个方法在WaitTable内部实现。

  • Task1是启动线程的主类。

电梯的启动与关闭

线程启动

我没有使用显式的Thread直接启动,而是使用ExecutorService管理线程,最后使用shutdown()退出执行

ExecutorService exec = Executors.newCachedThreadPool();
RequestReader reader = new RequestReader(elevatorInput, buildings);
exec.execute(reader);
for (int i = 0; i < 5; i++) {
  exec.execute(new Elevator(buildings.get(i), i, reader));
}

退出线程

这个问题一开始还困扰了我不少时间,因为不想让线程之间直接控制对方的完成,这样可能会造成很多安全问题,在不该停止线程的时候结束线程。最终我想到,两个线程之间的通讯工具就只有一个队列,这样RequestReader就可以put特殊的对象通知电梯没有输入,以便电梯最后停止运行。

building.put(new PersonRequest(0, 0, '0', '0', 1 ));

不同于第一次作业简单的线程关闭,之后的两次作业由于电梯之间的协作性,电梯的关闭反而成为了一个比较困难的问题,所以我之后复用了这种特殊信号传输的方法能够比较轻松的关掉电梯(~虽然由于信号维护的问题强测惨遭滑铁卢)。

协作时序图

电梯系统从create到结束(takeStop)的时序如上图所示,由于架构的简易,所以时序图比较清晰简单

评测机设计

由于有现成的请求代码,所以在解析输入的时候我直接使用了课程组的PersonRequest代码,唯一的改动是为了在评测的时候测试电梯移动的时间所以加上了时间戳的解析。

时间戳评判

首先需要进行的是时间戳判断,利用一个静态变量存储上一条输出的时间戳,将本条时间戳与上一条对比大小,判断是否时间戳递增,之后再维护静态变量的值,修改为本条的时间。

电梯的判断分为两个部分乘客移动逻辑和电梯移动逻辑。

乘客移动逻辑

  • In类型输出

    乘客移动逻辑先判断乘客状态,以防止同一个人多次进入电梯,乘客上电梯时的楼座与楼层是否和请求相同,然后改变乘客状态为Out。

    部分代码如下

    if (request.isIn()) {
                    System.out.println(request.toString() + "\n"  + outInfoP.toString() +  "\n"
                            + "Already in Elevator!");
                    return false;
                }
    if (request.getFromFloor() != outInfoP.getFloor()) {
      System.out.println(request.toString() + "\n"  + outInfoP.toString() +  "\n"
                         + "in floor error!");
      return false;
    }
    
  • Out类型输出

    先判断乘客状态,防止有造人的情况,再判断达到的楼座和楼层是否和请求一致。

    代码和In类型的判断相似。

  • 漏洞

    此版本的评测机当乘客有多次进出电梯的操作时会直接判错,该问题在之后迭代的版本中得到改善。

电梯移动逻辑

电梯移动的逻辑是一个比较麻烦的点,最终经过思考之后决定采取状态机完成。

  • 思路

    在内部对每一部电梯维护一个电梯类,电梯只有两个状态,开门或者关门,开门的时候能够进行关门操作,能够上下乘客,关门的时候电梯能够移动能够开门,其他所有操作均会引起错误。电梯内部还维护了乘客数量。

  • 状态转移

    接收一个电梯请求,先判断状态是否正确,然后判断和上一条信息的时间间隔是否合法,合法之后修改电梯状态,保存当前的电梯运动信息供下一次判断使用。

  • 乘客信息

    根据当前的电梯状态,只有在开门的时候会接受乘客信息,其他的操作都会报错,然后根据乘客的状态(OUT或IN)判断是否超载,并修改电梯的乘客数量。

最终判断

首先判断是否所有乘客都到达了正确的楼层(可以根据之前修改的状态判断),然后内部还进行了cpu时间判断(通过time命令获得)。

最终运行的python脚本如下

import os
def fun(i):
    print("testdata",i,":")
    os.system("rm stdin.txt")
    os.system("mv {0}.txt stdin.txt".format(i))
    os.system(" ./datainput_student_darwin | time -p -a -o stdout.txt java -jar Test.jar > stdout.txt ")
    if i <= 400:
        os.system("java -jar testMachine.jar")
    else: 
        os.system("echo 210.0 210.0 | java -jar testMachine.jar")
    os.system("mv stdout.txt out{0}".format(i))

os.system("java -jar DataGen.jar")
for i in range(8, 181):
        fun(i)


其中DataGen是数据生成器,由于三次作业的数据生成大致逻辑相同,构造技巧放在本文末。

BUG分析

自己的bug

本次作业公测和互测均未发现BUG,但是我的输出线程其实是不安全的,没有封装安全输出类,可能是因为架构比较好线程之间工作稳定,所以这次并未被发现bug,自己测试的时候也没出现问题,后面两次作业改正了这个漏洞。

最终强测得分 99.4027 ,有一个得分比较低的点是因为我没有在开关门的时候两次判断乘客是否需要进出,后面优化了这个点。

他人的bug

一个bug是有一位同学的输出线程不安全。

另一个bug是一位同学在remove的时候没有加锁,导致线程不安全,最终经过大量的聚集请求复现了(大概每5、6次复现一次)这个bug,交了五发之后评测机上复现了这个bug,不过重测的时候这个bug又消失了,十分神奇。

第六次作业

本次作业改变不大,横向电梯本质上是一个可循环的纵向电梯,所以需要改善的地方不多,大部分地方都是移植。

同步块的设置和锁的选择

本次作业由于涉及到自由竞争,所以当有新请求来的时候需要唤醒所有电梯,我查看了java源码发现原有的put方法只有拿到非空的成员之后才会将线程激活,所以现成安全类不太够,所以我写了puttake方法,来保证无论有没有拿到请求挂起的电梯都会被唤醒。

public synchronized PersonRequest take() throws InterruptedException {
        if (isEmpty()) {
            wait();
            return poll();
        } else {
            return poll();
        }
    }

    public synchronized void put(PersonRequest personRequest) throws InterruptedException {
        add(personRequest);
        notifyAll();
    }

本着线程之间尽量减少共享数据的原则,我只有这两个地方加了简单的锁。

调度器设置和调度策略

由于本次作业并没有本质上的调度区别,所以我这次作业依然没有设计调度器,但是由于有电梯新增的需求,所以新增了一个Controller来同一管理电梯的增加,并且将需求分发到各楼层各楼座。

调度策略依然采用look,纵向电梯自由竞争,横向电梯采用类look策略,具体逻辑如下

  • 电梯在上下来回扫描,期间能捎带就捎带。
  • 在同方向上没有请求之后电梯转向。
  • 同方向定义,由于电梯是循环的,所以将旋转方向上的前两座电梯作为同向判定,例如电梯顺时针选转,E的同向就是A、B两个楼座的乘客。

整体架构思路

本次作业的架构整体改变不大,只是新增了一些辅助类。

  • Controller:独立了RequestReader类的功能,仅仅专注于读入,整体架构更加合理,另外就是管理电梯的分配,并且在其中预留了接口,方便第三次作业的迭代。
  • WidthElevator:横向电梯,类似与纵向电梯,以judge方法判断电梯的移动。
  • WaitTable:纵向电梯的侯乘表,主要是为了实现横向look。

协作时序图

由于架构的修改,所以时序图有小的变化,主要是加入的Controller,由于第七次作业的架构没有区别,主要是调度策略的迭代,所以第六次和第七次作业共用时序和架构图。

评测机迭代

本次评测机迭代的难点是应对新增电梯的请求,由于第一版的评测机聚焦于电梯和乘客的逻辑,并未相到会有新增电梯的请求,所以评测机进行了小规模重构。

电梯增加请求

依旧抄了官方包的作业,省去了解析的麻烦,只是增加了时间戳的解析。在内部使用hashMap(id, Elevator)维护电梯,然后在评测机初始的时候,先增加基础的5+1座电梯,每次遇到电梯增加请求的时候再在hashMap当中增加电梯。

评测逻辑

横向电梯的评测逻辑大致和纵向电梯相同,需要注意的是速度的变化。

BUG分析

自己的bug

本次作业公测和互测均未发现BUG,反而在中测的时候第一次提交出现了一个小bug,原因是在第二次判断是否需要接乘客了时候漏了超载的问题。

最终强测得分97.8968 ,有一个点得到了85分,当时还比较疑惑,最后在写第七次作业的时候才发现,当电梯没有拿到请求时直接关闭了电梯,导致性能大受影响。也说明了后两次作业在性能分上有了一些轻视。

他人的bug

一个bug是有一位同学同一座电梯上了两次。

另外就是一位同学的也是遍历的时候没有加锁,导致的线程安全问题,和第五次作业的同学一样,但是复现的概率较低,本地大概20次复现一次,最终在评测机上没能复现。

第七次作业

同步块的设置和锁的选择

由于本次作业我想要通过计算加权值(速度,距离,聚集性)的方式,所以需要线程之间通信的地方明显增加,但是最多只有同类型的线程会共享数据,所以上锁的地方还是比较清晰。另外由于要关闭电梯(保证所有乘客都送到),所以此次的信号量也需要共享(最终酿成了强测寄点的惨案)。

一下两个地方是主要加锁的地方,numOfPer是监视信号量的数组。

synchronized (this) {
  numOfPer[personLi.getFromFloor() - 1]--;
  if (numOfPer[personLi.getFromFloor() - 1] == 0) {
    notifyAll();
  }
}
private synchronized boolean isEmpty() {
  for (int i : numOfPer) {
    if (i > 0) { return false; }
  }
  return true;
}

调度器设置和调度策略

本次作业的调度策略分两个方面,第一是乘客的接送依旧采用自由竞争的方式,另外就是中转楼层的选择,采用自己设计的公式进行加权值计算,最终选择权重较低的中转楼层。

加权方式介绍

基于减少乘客的换乘次数,电梯的聚集性,以及充分发挥速度的优势设计了以下简单的加权方式。

加权值base初始为0,之后根据以下规则在base上增加

  • 最小换乘原则

    保证乘客最多换乘两次,并且若横向电梯有和出发和达到楼层相同的,基础base = 0。

  • 电梯速度加权

    若是非最小换乘电梯,而且是最短距离电梯(例如出发到达楼层分别为6、10,则7-9是最短路径电梯),根据电梯耗费时间的比例关系,赋予加权值

    • 0.2s:+300
    • 0.4s:+600
    • 0.6s:+900
  • 开启聚集检测,聚集性是指侯乘的乘客以及电梯中的乘客多于该电梯所能接收的乘客

    • 0.2: + 100 * 多余人数

    • 0.4: + 150 * 多余人数

    • 0.6: + 200 * 多余人数

  • 尽量避免增加电梯运行距离的情况,根据楼层和电梯运行速度赋予加权值

    若是非最短距离电梯则根据一下规则增加加权值

    • 0.2 :+900 + 100 *( 绕行楼层)

    • 0.4 :+900 + 150 * (绕行楼层)

    • 0.5 :+900 + 200 * (绕行楼层)

整体架构思路

本次作业的架构整体几乎没有改变不大。主要的困难是在实现换乘请求的静态分割。

静态分割

使用PersonLink继承官方包的PersonRequest,先根据加权换乘原则实现静态分割,在内部保存该乘客的换乘状态,每次进入电梯或者出电梯都需要进行状态转换。

开门与关门的问题

本次作业由于需要换乘,之前直接发送关门信号的策略,可能会导致还没有接收到换乘乘客的电梯提前关门,最终导致乘客没有到达。所以我采用了监控开关门电梯信号量的方法。具体步骤如下

  • Controller收到结束信号时将该线程挂起

  • 维护一个数组num,其中数组下标代表对应楼层。

  • 每次静态分割时将对应楼层的num加一。

  • 每次换乘完成的时候就对应楼层num减一,并且当该楼层的请求变为0时唤醒Controller判断是否所有楼层的换乘信息是否清零,若不清零,则继续挂起线程,直到num清零才向各个电梯发送停止请求。

BUG分析

本地的bug

由于本次作业引入了算法,并且楼座停留有限制,所以出现了很多奇怪的问题。也预示了我强测要寄。典型的问题有两个。

  • 电梯吃人

    由于自由竞争,电梯接到了不能到达楼座的乘客,所以导致乘客一直关在电梯出不来。

  • 轮询

    也是由于没有有效判断,导致电梯一直反复请求不能接到的乘客,最终导致轮询。

自己的bug

本次作业惨遭滑铁卢,强测的时候因为电梯没关上寄了一个点,并且本地跑了四十多次也没有复现,好在我的架构比较清晰,和关闭电梯相关的地方也不多,在检查的代码之后我发现,由于当时我设想换乘信号的清零判断只于减少时有关,所以在增加换乘信号的时候为了性能我就没有加锁,所以导致清零的判断出错。后面仔细思考了这个bug,还是我自己对线程安全的掌握不够,没有清晰的判断信号之间的联系,为了一点性能导致更大的错误,最终强测分数只有 93.4974。

他人的bug

本次作业一个同学的bug比较明显,是ArrayList越界,另外一个同学也是电梯关门问题,当时间跨度较大时它的电梯无法正常关门。

HACK策略与数据构造

数据构造

数据生成思路

根据本次作业要求,新增了添加电梯以及横向电梯请求的相关数据,所以数据生成的总体架构分为三个部分,一个是纵向电梯数据生成以及控制时间戳,以及横向电梯数据生成添加电梯数据生成。

纵向电梯数据生成及控制

数据生成器接口

由于电梯对数据读输入有格式化要求,所以要产生有效的数据并不困难,但是为了产生不同的强度,不同针对性的数据,以及为了控制整改数据生成的时间戳,我所有的数据生成器都实现了一个接口,针对不同的情况再重写接口的方法。

public interface Generator {
    String getRequest();
    void setNowTime(double nowTime);
    double getNowTime();
    double getTime();
    int getPersonId();
    boolean judgeTime();
}

其中getRequest()方法是返回一个字符串,是获得请求的方法,其他方法是控制添加电梯,以及添加横向电梯的方法。

内部生成即控制逻辑
  • 生成逻辑:数据生成的逻辑是以Random获得随机数据,以控制电梯的出发楼座,达到楼座,出发楼层,到达楼层等四个关键量,并且在Generator的内部会保存上一条数据的相关数据,以便后续进行并发程度较高,聚集性较大的数据构造。
  • 时间戳维护:Generator内部维护了一个lasttime变量,在每次生成数据的时候根据要求随机递增或不递增,一直到一整条数据生成的周期之后根据要求重置时间戳,这个时间戳在生成横向电梯数据以及增加电梯的时候可以共用。
  • 数据控制逻辑:通过nextInt(bound)方法的bound参数控制某个数据生成的概率,以构造高并发数据,例如控制一个数据和上一条数据的楼座有4/5的概率相同可以采用以下代码。
  char fromBuilding = lastBui != 0 &&
                  random.nextInt(10) > 1 ? lastBui : (char)(random.nextInt(5) + 'A');

最终一个普遍的随机性数据生成的完整代码如下

  @Override
      public String getRequest() {
          // 随机性较强的数据
          char fromBuilding = (char)(random.nextInt(5) + 'A');
          int fromFloor = random.nextInt(10) + 1;
          int temp = random.nextInt(10) + 1;
          while (temp == fromFloor) { temp = random.nextInt(10) + 1;}
          int toFloor = temp;
          nowTime += (double) random.nextInt(30) / 10;
          return String.format("[%.1f]%d-FROM-%c-%d-TO-%c-%d",
                  Math.min(nowTime, 70.0), personId++, fromBuilding, fromFloor, fromBuilding, toFloor);
      }

添加电梯增加数据生成

  • 时间戳:时间戳已经在Generator内部维护,每次数据生成之前只需要通过getNowTime()方法获得时间即可,为了满足课程组的数据要求,每次生成数据之后使时间戳增加1s。
  ElevatorGen.setLasttime(generator.getNowTime());
  • 纵向电梯:纵向电梯两个重要的参数是id楼座,其中id独一无二,所以可以用Hashmap<Integer, Character>存入添加的电梯,避免id添加的冲突。
  • 横向电梯:横向电梯两个重要的参数是id楼层,同样以id为key存入Hashmap<Integer, Character>中。
  • 生成逻辑:同样以Random作为数据选择器,以上文所提到的概率控制方法,保存上一条的信息,来生成高并发的数据。需要注意的是,每一次数据生成结束之后要重置两个Hashmap

数据生成的代码如下

if (random.nextInt(2) == 0) {
            int floor = random.nextInt(10) + 1;
            int id = random.nextInt(10000);
            while (widthE.containsKey(id) || commonE.containsKey(id)) {
                id = random.nextInt();
            }
            widthE.put(id, floor);
            num++;
            return String.format("[%.1f]ADD-floor-%d-%d", lasttime, id, floor);
        } else {
            char building = (char)(random.nextInt(5) + 'A');
            int id = random.nextInt(10000);
            while (widthE.containsKey(id) || commonE.containsKey(id)) {
                id = random.nextInt();
            }
            commonE.put(id, building);
            num++;
            return String.format("[%.1f]ADD-building-%d-%c", lasttime, id, building);
        }

横向电梯数据生成

  • 时间戳:时间戳已经在Generator内部维护,每次数据生成之前只需要通过getTime()方法获得时间即可。
  • 生成逻辑:生成逻辑和纵向电梯数据生成逻辑一致,只是横向电梯需随机选择的是楼座。另外在生成数据之前需要访问,电梯的Hashmap在已有的横向电梯里增加数据。
  static public int getFloor(Random random) {
          ArrayList<Integer> list = new ArrayList<>(widthE.keySet());
          return widthE.get(list.get(random.nextInt(list.size())));
      }

完整代码如下

public String getRequest(double nowTime, int personId) {
        char fromBuilding = lastBui != 0 ?  lastBui : (char)(random.nextInt(5) + 'A');
        char toBuilding = (char)(random.nextInt(5) + 'A');
        while (toBuilding == fromBuilding) {
            toBuilding = (char)(random.nextInt(5) + 'A');
        }
        int fromFloor = ElevatorGen.getFloor(random);
        return String.format("[%.1f]%d-FROM-%c-%d-TO-%c-%d",
                Math.min(nowTime, 70.0), personId, fromBuilding, fromFloor, toBuilding, fromFloor);
    }

最终将同一时间戳下的数据整合即可获得完整的一个数据

generator.setNowTime(random.nextInt(50) + 1);
            System.setOut(new PrintStream(i + ".txt"));
            ElevatorGen.reset();
            for (int k = 0; k < 70; k++) {
                int op = random.nextInt(5);
                if (op == 0 && !ElevatorGen.isFull() && !generator.judgeTime()) {
                    ElevatorGen.setLasttime(generator.getNowTime());
                    System.out.println(ElevatorGen.addElevator(random));
                } else if (op < 3 && !ElevatorGen.isEmpty()) {
                    System.out.println(getWidth(generator, widthGen));
                } else {
                    System.out.println(generator.getRequest());
                }
            }

针对性数据构造

功能性测试数据

此部分的数据主要测试电梯运行的功能(是否存在越界,逻辑错误等等),所以主要是以随机生成的数据为主,无需人为干涉产生的概率,时间戳也比较分散

一定聚集性的数据

此部分通过缩短时间戳,以及增加同一楼座以及同一楼层的数据出现的概率来测试电梯的线程安全、超载等等可能出下的问题,具体控制代码如下。

char fromBuilding = lastBui != 0 &&
  random.nextInt(10) > 1 ? lastBui : (char)(random.nextInt(5) + 'A');
int fromFloor = lastBui == fromBuilding && random.nextInt(10) > 1?
  lastFloor : random.nextInt(10) + 1;
nowTime += (double) random.nextInt(10) / 10;

时间跨度控制在1s以内,和上一条数据楼座和楼层相同的概率均为4/5,具体效果如下。

[60.6]1-FROM-E-7-TO-E-6
[61.5]2-FROM-E-7-TO-E-8
[62.1]3-FROM-E-7-TO-E-3
[62.5]4-FROM-E-7-TO-E-2
[62.6]5-FROM-E-7-TO-E-10
[62.6]ADD-building-6033-D
[63.9]6-FROM-B-10-TO-B-8
[64.6]7-FROM-B-10-TO-B-3
[65.5]8-FROM-B-10-TO-B-2
...
同一电梯大量聚集

当多部电梯同时工作的时候,各个电梯之间并无影响,各个线程之间互不干扰,所以当所有请求针对同一电梯时,对单个电梯的调度等要求更高,有问题的电梯出现错误的概率也会增加,所以该部分的数据针对同一电梯进行数据构造,缩短时间轴,测试电梯是否能够正常工作。

char fromBuilding = lastBui != 0 &&
                random.nextInt(100) > 1 ? lastBui : (char)(random.nextInt(5) + 'A');
int fromFloor = lastBui == fromBuilding && random.nextInt(100) > 1?
                              lastFloor : random.nextInt(10) + 1;
nowTime += (double) random.nextInt(5) == 1 ? 0.1 : 0;

时间跨度控制在0.1s以内,和上一条数据楼座和楼层相同的概率均为49/50。

针对电梯调度策略的数据

同理当所有请求针对同一电梯的时候,电梯运行时间会最大化,为了在互测当中能够成功hack,还可以通过将时间戳提升到70.0s以达到电梯系统结束时间的最大化,概率控制和上一条数据相似。

针对输出线程安全的数据

我们知道,当多个线程同时进行输出时,可能造成线程输出的问题,所以这个时候需要增加线程的数量,缩短时间轴,来保证短时间内能有大量输出,如果输出线程有问题,此时有较大概率出现时间时间戳不递增。实现方法可以通过增加电梯数量,并且各个楼座均有一定数量数据。

针对电梯线程安全问题的数据

上一次互测的时候,我发现一位同学的线程抛出了异常,最后查看代码发现,它的remove()方法没有加锁,所以若是在remove的时候有请求进入会修改迭代器出现ConcurrentModificationException异常。根据电梯的设计,开门的时候会出现remove的情况,所以我们可以在开门时间,开门时间+0.2,开门时间+0.4 开门时间+0.6四个时间点投入大量同一楼座的数据,来检测是否会发生上述异常。

针对电梯停止的数据

此部分数据主要为了测试测试电梯数量达到最大时,若是没有请求,电梯能否正常关门。

HACK策略

本次hack依旧采用黑盒的方式hack,将同学的代码丢入评测机,然后一段时间查看看评测结果即可,具体脚本代码如下。

import os
def fun(i):
    print("testdata",i,":")
    os.system("rm stdin.txt")
    os.system("mv {0}.txt stdin.txt".format(i))
    os.system(" ./datainput_student_darwin | time -p -a -o stdout.txt java -jar Test.jar > stdout.txt ")
    if i <= 400:
        os.system("java -jar testMachine.jar")
    else: 
        os.system("echo 210.0 210.0 | java -jar testMachine.jar")
    os.system("mv stdout.txt out{0}".format(i))

os.system("java -jar DataGen.jar")
for i in range(8, 181):
        fun(i)

心得体会

收获

本单元的作业投入了大量的时间,付出很大收获也颇丰。

多线程的安全问题

本次作业查阅了大量资料,包括但不限于黑书、往届学长的博客,所以对整个多线程的问题有了一个基本清晰的了解,但是依然有一些花里胡哨的东西没有用上,例如原子类。

设计架构

这是本单元我最满意的部分,由于惯性思维,本人从小到大做事都比较随性,很多时候不能形成一个清晰的设计,导致我第一单元的崩盘。所以我第二单元就做好了在设计上下大功夫的准备,最终的成果还是比较满意,大大减少了我实现时候的困难,并且也很少有一些奇奇怪怪的bug。

评测机设计

由于电梯的多状态,所以本单元的作业评测机的设计是一个难点,电梯不断迭代的同时,评测机也在不断的迭代,最终迭代完成的版本,代码量甚至超过电梯本身,这对我本身设计状态机以及测评的能力有了很大的提高。

数据构造能力

本单元作业随机构造的数据就具有一定的强度,但是在不断发现BUG的过程中数据构造也在根据各种奇怪的bug进行迭代,本次作业主要是这种针对性构造数据的能力得到了成长。

不足之处

本单元作业虽然投入了大量时间,但是能力以及设计上的不足之处依然客观的存在,主要总结为以下几点。

多线程的理解

本单元作业唯一被发现的一个bug就来自于对多线程理解的不深刻,最终导致一个该加锁的地方没有加锁,说明在复杂线程下完全避免线程安全的能力还没有达到要求。

JAVA编译

由于没有仔细研究过jdk的命令行等用法,所以本单元作业打包都还是采用手动的方式,增加了不少麻烦,我感觉这是急需提升的能力。

PYTHON能力

由于想模拟评测机80路并发的场景,所以我在本地想利用python的多线程实现,但是由于本人的python能力实在太烂,最终没能有效实现,未能实现高并发评测。

算法能力

算法能力也是我比较薄弱的地方,第七次作业由于设计了较为复杂的计算系统,所以算法的复杂度较高,导致我的cpu时间明显较高(在解决轮询问题之后)。

课程建议

和去年电梯作业的对比,我体会到了各位助教工作的用心,但是我的感觉是课程组一直想要考察学生在调度上的创新,但是根据我浅薄的眼光了解到,似乎自由竞争的细节能够做的好就能拿到高分,所以希望明年的题目可以设计更加刁钻的题目以体现调度策略的重要性。例如增加电梯运行速度的差别(相对于开关门时间),增加允许的乘客数,增加更多种类的电梯等等。

posted @ 2022-05-04 00:05  shliba  阅读(121)  评论(1编辑  收藏  举报