OO Unit2多线程电梯总结博客

OO Unit2多线程电梯总结博客

传说中的电梯居然就这样写完了~撒花🎉给今年的课程组笔芯,提供了输入输出接口包,不用像以前一样麻烦地处理输入,改善了面向过程和面向Wrong Format的程序设计。

 

程序设计思路和度量分析

三次作业都贯穿着一对函数:

Crawl & Slide

我的所有的程序一个中心,两个基本点,就是每次调度器会从队列里拿出一个“主请求”,如果这个主请求是向上走的,那么使用Crawl(int from, int to)函数,如果是向下走的,则使用Slide(int from , int to )函数;其中fromto是这个主请求的出发楼层和到达楼层。Crawl和Slide是贯穿始终的两个基本点。

在这两个函数中,使用一个循环,每到达一个楼层之前,Sleep对应的运行时间,然后将电梯属性onThisFloor(int floor)设置到这个楼层(第二第三次的电梯还要输出“ARRIVE”,不可以直接Sleep一个很长的时间然后摸鱼)。此时先看看电梯里是不是有人,如果有人在这一层楼下,如果门是关着的,那么先开门,然后输出OUT;乘客出去之后,在遍历队列看看有无人在这一层楼上电梯,(同样若门是关着的,先开门)若有人下则输出IN。最后,若门的状态是打开的,则输出关门CLOSE操作。一定要避免重复输出开关门信息。

傻瓜调度 - FCFS

先来先服务,严格按照顺序,一次只能运送一个人,没有性能可言

类的设计 - 傻瓜调度电梯

五个类:

  • MainClass - 主函数,处理输入

  • Elevator - 只是一个电梯厢,不会自己跑,全靠调度器安排

  • Person - 保存有ID,出发楼层,目标楼层,以及访问这些变量的方法

  • Dispatch - 调度器,包含电梯厢,人,请求队列,只有一个run任务是要在特定时间输出信息的。

  • TaskQueue - 在LinkedBlockingQueue<>基础上运用了一些方法,取头元素,判断是否已满或为空等等

 

傻瓜电梯的Complexity Metrcis

分析结果中可以看到ev, iv, v这几栏,分别代指基本复杂度(Essential Complexity (ev(G))、模块设计复杂度(耦合度)(Module Design Complexity (iv(G)))、Cyclomatic Complexity (v(G))圈复杂度(独立路径的条数)。OCavgWMC两个项目,分别代表类的方法的平均循环复杂度和总循环复杂度。

Methodev(G)iv(G)v(G)
Dispatch.Dispatch(Elevator) 1 1 1
Dispatch.NewRequest(PersonRequest) 1 1 1
Dispatch.Work() 1 2 2
Dispatch.run() 1 2 2
Elevator.Elevator(int) 1 1 1
Elevator.Run() 1 1 1
Elevator.Stop() 1 1 1
Elevator.isClosed() 1 1 1
Elevator.isOpen() 1 1 1
Elevator.isRunning() 1 1 1
Elevator.onThisFloor(int) 1 1 1
Elevator.onWhichFloor() 1 1 1
MainClass.main(String[]) 3 3 3
Person.Person(int,int,int) 1 1 1
Person.getDstFloor() 1 1 1
Person.getId() 1 1 1
Person.getSrcFloor() 1 1 1
Person.goIn() 1 1 1
Person.goOut() 1 1 1
Person.isInElevator() 1 1 1
TaskQueue.getQueue() 1 1 1
TaskQueue.isEmpty() 1 1 1
TaskQueue.isFull() 1 1 1
TaskQueue.pushELm() 1 1 1
TaskQueue.putIntoQueue(PersonRequest) 1 1 1
ClassOCavgWMC
Dispatch 1.25 5
Elevator 1 8
MainClass 3 3
Person 1 7
TaskQueue 1 5

傻瓜电梯的Dependency Metrcis

依赖度分析度量了类之间的依赖程度。有如下几种项目:

  1. Cyclic:指和类直接或间接相互依赖的类的数量。这样的相互依赖可能导致代码难以理解和测试。

  2. Dcy和Dcy*:计算了该类直接依赖的类的数量,带*表示包括了间接依赖的类。

  3. Dpt和Dpt*:计算了直接依赖该类的类的数量,带*表示包括了间接依赖的类。

ClassCyclicDcyDcy*DptDpt*
Dispatch 0 3 3 1 1
Elevator 0 0 0 2 2
MainClass 0 2 4 0 0
Person 0 0 0 1 2
TaskQueue 0 0 0 1 2

总的来说,傻瓜调度的电梯还是非常简单的,因为没有性能要求,我史无前例地(之后大概也不会)在强测中拿了100分。

 

捎带 - ALS

取出主请求,在上下运行的过程中如果有同一个方向的,然后到达了该楼层是电梯内某人的目的地,则下来,该楼层是upQueue或downQueue的其中一个人的始发地,则上车。

类的设计 - 可捎带电梯

  • MainClass - 主函数,处理输入

  • Elevator - 只是一个电梯厢,不会自己跑,全靠调度器安排

  • Person - 保存有ID,出发楼层,目标楼层,以及访问这些变量的方法和各种人的状态

  • Dispatch - 调度器,包含电梯厢,人,包含一个上行队列upQueue和一个下行队列downQueue,有最重要的crawl和slide方法,输出信息在Dispatch中完成

  • TaskQueue - 改成了以ArrayList为基础的队列,可在任意位置插入人,可以判断是否为空或满

类的设计与第一次作业基本保持一致,但是功能有一些变化,给调度器的职责更重了

(这个图画起来有一点专门为人服务的感觉哈哈哈啊)

ALS Complexity Metrcis

Methodev(G)iv(G)v(G)
Dispatch.Dispatch(Elevator) 1 1 1
Dispatch.NewRequest(PersonRequest) 1 3 3
Dispatch.canEnd() 2 4 5
Dispatch.carryLeftPeople(Elevator,Person) 1 3 3
Dispatch.closeDoor(Elevator,int) 1 2 2
Dispatch.crawl(int,int) 11 20 21
Dispatch.getMain(Elevator,TaskQueue,TaskQueue) 1 7 7
Dispatch.run() 1 2 2
Dispatch.slide(int,int) 11 20 21
Dispatch.work() 6 14 14
Elevator.Close() 1 1 1
Elevator.Elevator(int) 1 1 1
Elevator.Open() 1 1 1
Elevator.Run() 1 1 1
Elevator.Stop() 1 1 1
Elevator.getPerson(int) 1 1 1
Elevator.goIn(Person) 1 1 1
Elevator.goOut(Person) 1 1 1
Elevator.hasPeople() 1 1 1
Elevator.isClosed() 1 1 1
Elevator.isGoDown() 1 1 1
Elevator.isGoUp() 1 1 1
Elevator.isOpen() 1 1 1
Elevator.isRunning() 1 1 1
Elevator.onThisFloor(int) 1 1 1
Elevator.onWhichFloor() 1 1 1
Elevator.peopleNum() 1 1 1
Elevator.setGoDown() 1 1 1
Elevator.setGoUp() 1 1 1
MainClass.main(String[]) 3 5 5
Person.Person() 1 1 1
Person.Person(int,int,int) 1 1 3
Person.getDirection() 1 1 1
Person.getDstFloor() 1 1 1
Person.getId() 1 1 1
Person.getSrcFloor() 1 1 1
Person.goIn() 1 1 1
Person.goOut() 1 1 1
Person.isGoingDown() 1 1 1
Person.isGoingUp() 1 1 1
Person.isInElevator() 1 1 1
TaskQueue.getQueueElm(int) 1 1 1
TaskQueue.insertQueue(Person,int) 9 9 9
TaskQueue.isEmpty() 1 1 1
TaskQueue.isFull() 1 1 1
TaskQueue.kickOut(Person) 1 1 1
TaskQueue.pushELm() 1 1 1
TaskQueue.putQueueHead(Person) 1 1 1
TaskQueue.putQueueTail(Person) 1 1 1
TaskQueue.size() 1 1 1
ClassOCavgWMC
Dispatch 5.5 55
Elevator 1 19
MainClass 5 5
Person 1.18 13
TaskQueue 1.89 17

ALS Dependency Metrcis

ClassCyclicDcyDcy*DptDpt*
Dispatch 0 3 3 1 1
Elevator 0 1 1 2 2
MainClass 0 2 4 0 0
Person 0 0 0 3 4
TaskQueue 0 1 1 1 2

 

 

多电梯 - SS

我的设计思路有以下几点:

  1. 先判断是否能直达,若能直达,Dispatch把任务放到对应电梯的队列里,优先分配A电梯,其次B、C。 若不能直达,则给出换乘方案:把两段任务都先直接放到放到相应的电梯队列里,但是第二个任务的Validity为False,即便电梯到了他的出发楼层,他也不能“进入电梯”。

  2. 通过计算,到距离这个人最近的换乘点进行换乘,到达换乘点后,若发现这个人是一个换乘的前半段任务,那么把第二个人的Validity设置为True,他就可以在Crawl和Slide中上电梯了。

  3. 每个电梯只能从自己的队列里取任务,不能关心外面的Dispatch和InputHandler,上下过程中可捎带。

  4. 所有输出信息都写在Elevator当中,剩下可以安心运行而不关心琐碎的事情,在for循环中Arrive,Crawl是向上运行,Slide是向下运行。进进出出,开门关门都是电梯的事情。

  5. 换乘人在第一阶段运行中,输入依然可以正确进行,只要不输入Command + D

  6. 如果主请求运输完毕后,电梯会先将被捎带还在电梯内的乘客送到目的地,然后再取新的请求。

类的设计 - 多电梯

  • MainClass - 主函数,启动一个输入线程和一个调度器线程

  • Elevator - 一个电梯厢,包含一个上行队列upQueue和一个下行队列downQueue,本身是一个Thread,包含所有的输出信息;调度器只负责往电梯的队列里塞人,电梯需要从自己的队列里取用需求,并且上下运动捎带,有最重要的crawl和slide方法

  • Person - 保存有ID,出发楼层,目标楼层,以及访问这些变量的方法和各种人的状态;

    增加了Ancestor和exchange变量,如果exchange为true,那么表明有换乘的“前人”,通过检测Ancestor的楼层状态可以判断这个人是否能够进入电梯

  • Dispatch - 调度器,包含电梯厢,人,主队列,启用三个电梯线程决定某人请求是否需要换乘,且应该放到哪个电梯的哪个队列里。

  • TaskQueue - 以ArrayList为基础的队列,可在任意位置插入人,可以判断是否为空或满

  • InputHandler - 单独开一个线程来处理输入,并将需求传递给Dispatch

 

SS Multielevator Complexity Metrcis

Methodev(G)iv(G)v(G)
multielevator.Dispatch.AllStop() 2 3 4
multielevator.Dispatch.Dispatch(Elevator,Elevator,Elevator) 1 1 1
multielevator.Dispatch.canStop() 1 1 1
multielevator.Dispatch.nearExchangeab(int) 5 1 7
multielevator.Dispatch.nearExchangeac(int) 2 1 2
multielevator.Dispatch.nearExchangebc(int) 8 1 10
multielevator.Dispatch.putExchangePerson(Person,int,int,int,Elevator,Elevator) 1 3 3
multielevator.Dispatch.putInPerson(Person) 1 14 14
multielevator.Dispatch.putStraight(Person,int,int) 4 7 7
multielevator.Dispatch.reachA(int) 2 1 3
multielevator.Dispatch.reachB(int) 2 1 5
multielevator.Dispatch.reachC(int) 2 1 2
multielevator.Dispatch.run() 1 1 1
multielevator.Dispatch.straight(int,int) 4 6 7
multielevator.Dispatch.work() 1 1 1
multielevator.Elevator.Arrive(int) 1 1 1
multielevator.Elevator.Close(int) 1 2 2
multielevator.Elevator.Crawl(int,int) 12 20 22
multielevator.Elevator.Elevator(String,int,int) 1 1 1
multielevator.Elevator.Open(int) 1 2 2
multielevator.Elevator.PickUp(int,int) 1 4 4
multielevator.Elevator.Run() 1 1 1
multielevator.Elevator.Slide(int,int) 12 20 22
multielevator.Elevator.Stop() 1 1 1
multielevator.Elevator.allEmpty() 2 5 6
multielevator.Elevator.carryLeftPeople() 1 3 3
multielevator.Elevator.getMain() 1 7 7
multielevator.Elevator.getPerson(int) 1 1 1
multielevator.Elevator.goIn(int,Person) 2 2 2
multielevator.Elevator.goOut(int,Person) 1 1 1
multielevator.Elevator.hasPeople() 1 1 1
multielevator.Elevator.isClosed() 1 1 1
multielevator.Elevator.isGoDown() 1 1 1
multielevator.Elevator.isGoUp() 1 1 1
multielevator.Elevator.isOpen() 1 1 1
multielevator.Elevator.isRunning() 1 1 1
multielevator.Elevator.onThisFloor(int) 1 1 1
multielevator.Elevator.onWhichFloor() 1 1 1
multielevator.Elevator.peopleNum() 1 1 1
multielevator.Elevator.putInDown(Person) 1 1 1
multielevator.Elevator.putInUp(Person) 1 1 1
multielevator.Elevator.run() 1 17 17
multielevator.Elevator.setGoDown() 1 1 1
multielevator.Elevator.setGoUp() 1 1 1
multielevator.Elevator.setStopSignal() 1 1 1
multielevator.InputHandler.InputHandler(Dispatch) 1 1 1
multielevator.InputHandler.run() 3 8 8
multielevator.MainClass.main(String[]) 1 1 1
multielevator.Person.ArriveDst() 1 1 1
multielevator.Person.Person() 1 1 1
multielevator.Person.Person(int,int,int) 1 1 3
multielevator.Person.getDirection() 1 1 1
multielevator.Person.getDstFloor() 1 1 1
multielevator.Person.getId() 1 1 1
multielevator.Person.getSrcFloor() 1 1 1
multielevator.Person.goIn() 1 1 1
multielevator.Person.goOut() 1 1 1
multielevator.Person.hasArrivedFirst() 3 2 3
multielevator.Person.isExchange() 1 1 1
multielevator.Person.isGoingDown() 1 1 1
multielevator.Person.isGoingUp() 1 1 1
multielevator.Person.isInElevator() 1 1 1
multielevator.Person.isOnThisFloor(int) 1 1 1
multielevator.Person.needExchange(Person) 1 1 1
multielevator.TaskQueue.getQueueElm(int) 1 1 1
multielevator.TaskQueue.isEmpty() 1 1 1
multielevator.TaskQueue.isFull() 1 1 1
multielevator.TaskQueue.kickOut(Person) 1 1 1
multielevator.TaskQueue.putQueueHead(Person) 1 1 1
multielevator.TaskQueue.putQueueTail(Person) 1 1 1
multielevator.TaskQueue.size() 1 1 1
ClassOCavgWMC
multielevator.Dispatch 3.53 53
multielevator.Elevator 2.47 74
multielevator.InputHandler 3 6
multielevator.MainClass 1 1
multielevator.Person 1.25 20
multielevator.TaskQueue 1 7

我把所有源文件放在了一个elevator包里。有一些方法复杂度还是居高不下,尤其是核心函数Crawl和Slide对人、楼层、队列的操作较多,导致了圈复杂度大。当总体上还是比较明了的。

 1 public void Crawl(int from, int to) throws InterruptedException {
 2          for (int i = from; i <= to; i++) {
 3              if (i == 0) { continue; } //没有第0层
 4              this.onThisFloor(i);
 5              if (i > from) {
 6                  Thread.sleep(this.runningTime);
 7                  this.Arrive(i); // OUTPUT
 8              }
 9              //先检查有无人下电梯
10              for (int k = 0; k < this.peopleNum() && this.hasPeople();) {
11                  Person p = this.getPerson(k);
12                  while (p.getDstFloor() == i) {
13                      // 放人下来
14                      this.Open(i);
15                      this.goOut(i,p); // 走出电梯
16                      p.ArriveDst(); // 人 p 到达楼层
17                      if (p.isExchange()) { p.hasArrivedFirst(); } // ancestor释放到达信号
18                      if (!this.hasPeople()
19                              || k >= this.peopleNum() - 1) {
20                          if (k == 0 && this.hasPeople()) {
21                              p = this.getPerson(0);
22                              continue;
23                          } else {
24                              break;
25                          }
26                      }
27                      p = this.getPerson(k);
28                  }
29                  k++;  // 继续看下一个人要不要出电梯
30              }
31              // 检查有无人上电梯
32              for (int j = 0; j < this.upQueue.size()
33                      && !this.upQueue.isEmpty(); ) {
34                  //System.out.println("Upqueue size is " + this.upQueue.size());
35                  Person p = this.upQueue.getQueueElm(j);
36                  while (p.getSrcFloor() == i && p.isGoingUp()
37                          && p.hasArrivedFirst()) {
38                      if (this.peopleNum() == this.maximum) { break; }
39                      // 有人上行且在该楼层出发,停下接人
40                      this.Open(i);           // 输出对应楼层开门信息
41                      this.goIn(i,p);         // 人走进电梯
42                      this.upQueue.kickOut(p); // 一旦进入电梯,则从队列中KickOut
43                      if (this.upQueue.isEmpty()
44                              || j >= this.upQueue.size() - 1) {
45                          if (j == 0 && !this.upQueue.isEmpty()) {
46                              p = this.upQueue.getQueueElm(0);
47                              continue;
48                          } else {
49                              break;
50                          }
51                      }
52                      p = this.upQueue.getQueueElm(j);
53                  }
54                  j++; // 继续看下一个人要不要进电梯
55              }
56              this.Close(i);
57          }
58      }

 

 

SS Multielevator Dependency Metrcis

ClassCyclicDcyDcy*DptDpt*
multielevator.Dispatch 0 3 4 2 2
multielevator.Elevator 0 2 2 2 3
multielevator.InputHandler 0 2 5 1 1
multielevator.MainClass 0 3 6 0 0
multielevator.Person 0 0 0 4 5
multielevator.TaskQueue 0 1 1 1 4

可以看出,即使到了第三次作业多部电梯,接相互依赖的类的数量也都是0,在类的安排和设计上还是比较妥当的。

 

分析自己程序的Bug

由于各种因素,前两次作业大家Hack兴致并不是很浓厚,没有被Hack。

Bug主要特征:

Time Limit Exceed 超时

在第二次作业中,我有一个强测点超时,是往队列里头插入导致的,后来全部改成在队尾插入就OK了。

在第三次作业中,有两个强测点超时,原因是我智障地在换乘人乘坐第一阶段电梯时,当场Sleep轮询看此人是否到达目的地,直接阻塞了输入,导致用时特别长。在Dispatch类中修复后,改成了同时把两阶段需求放入电梯。四十几条指令原本需要220秒的修复后80秒可跑完;原本需要163秒的60秒即可跑完。

还有一个小问题就是在同一时间同时输出了相同信息,大概是输出线程不安全的问题,把它锁起来以后解决。

bug位置与设计结构之间的相关性

 

多线程安全&各种坑

Wait() & NotifyAll

  • 永远在synchronize块中(对竞争资源进行加锁)和那个被多线程共享的对象上调用wait()

  • 永远在while循环中而不是if语句中调用wait()

  • 初学者尽量使用NotifyAll(),而不是Notify()

如果长时间没有输入的话,可以用wait()等待请求的到来,而不是sleep轮询

 
 1   public synchronized Person getMain() { // 获取主请求的函数
 2          Person p = new Person();
 3          if (!this.hasPeople()) { // 如果电梯没有人
 4              while (this.upQueue.isEmpty() && this.downQueue.isEmpty()) {
 5                  try { // 如果上行和下行的队列都是空的,那么等待
 6                      wait();
 7                  } catch (Exception e) {
 8                      System.out.println("ALL EMPTY ELEVATOR AND QUEUE");
 9                  }
10              } // 则从最早到队列中取出任务
11              if (!this.upQueue.isEmpty()) { //先往上走
12                  p = this.upQueue.getQueueElm(0);
13              } else if (!this.downQueue.isEmpty()) {
14                  p = this.downQueue.getQueueElm(0);
15              }
16          } else {
17              p = this.getPerson(0);
18          }
19          return p;
20      }
21     public synchronized void putInUp(Person person) {
22          this.upQueue.putQueueTail(person);
23          notifyAll(); // 在往电梯里放入请求的时候notifyall唤醒刚才wait() 的线程即可
24      } 
25  

 

Final修饰符

善用final修饰一旦赋值就不能再修改的变量,可以避免很多不必要的玄学bug,两种情况:

  1. 定义的时候直接赋值

  2. 定义的时候未赋值,在构造器中赋值

比如三个电梯不同的ID,最大荷载人数,不同的单层运行时间,开关门的时间,都可以用final来修饰

1  private final String id;
2  private final int maximum;
3  private final int runningTime;
4  private final int openCloseTime = 200;
5  public Elevator(String s, int max, int time) {
6          this.id = s;
7          this.maximum = max;
8          this.runningTime = time;
9      }

 

 

输出安全

这次的TimableOutput据说是线程不安全的(?)我出现过同一时间输出两条完全相同的语句,连时间戳也是相同的。为了保险起见,把每一个输出都用synchronize锁起来:

  

1  synchronized (TimableOutput.class) {
2              TimableOutput.println(
3                      String.format("OUT-%d-%d-%s",p.getId(),floor,this.id));
4          }

 

Hack

策略:盲测

在第二次测试中成功Hack他人5次。没有结合被测程序的代码设计结构来设计测试用例。

发现线程安全相关的问题:

 6022222-FROM--3-TO-15
 23333333-FROM--3-TO-14
 12345678-FROM--2-TO-13
 798-FROM-16-TO--1

该样例有同学输出一条信息后便卡住了,有一个人会在人还没进电梯之前就输出了OUT。

 

本单元的测试策略与第一单元测试策略的差异之处

本单元测试的可变性很多很复杂,正确解的形式五花八门,并且不像第一单元的表达式那样还有现成的工具可以化简判断正误。所以我只能小规模、10条一下的指令进行测试,没有进行压力测试。(我又不会像大佬那样写一个测评机出来TAT)

 

最后的感想

  • 多学习大佬们的博客,不需要从头造轮子,注意代码可扩展性

  • 看看《图解Java多线程设计模式》,不要从心理上惧怕多线程,战略上藐视它,战术上重视它

  • 先做好充足的思考可以减少编码时间,画个思维导图

  • 这次的电梯任务总算是面向对象了,把所有的类都看成一个实体,每个对象做好自己该做的事情

posted @ 2019-04-23 16:20  Vanellope  阅读(380)  评论(0编辑  收藏  举报