电梯作业总结

程序结构与复杂度的分析

第一次作业

1.设计思路

第一次作业是电梯作业的第一次,也是我多线程变成的第一次实践。任务是编写一个多线程实时电梯系统,采用FAFS的调度方式。由于第一次作业中没有涉及到多部电梯以及捎带的情况,因此来说是比较简单的。我采用的是指导书提示部分中的模式,即生产者消费者模式

  • 主线程进行输入的管理,使用ElevatorInput,负责接收请求并存入队列
  • 开一个线程,用于模拟电梯的活动,负责从队列中取出请求并进行执行,执行完后继续取继续执行
  • 构建一个队列,用于管理请求,且需要保证队列是线程安全的
ElevatorInputHanler

在第五次的作业中,我将主线程main直接作为了输入管理进程,原因是看到了知道书中的主线程进行输入的管理,使用ElevatorInput,负责接收请求并存入队列

Request

在理解了题意和指导书中的模式推荐,我觉得这和我之前在《设计模式——可复用面向对象软件的基础》这本书中看到的生产者——消费者模式相近,故我采用了该模式来编写Request类

Elevator

Elevator类就是电梯类,内部保存着电梯的状态(如当前楼层),主要完成电梯在接收到请求之后的上下路以及开关门的操作

整体架构图如下:

2.度量分析

(1)复杂度分析

从图中可以看出在第一次电梯作业中,我的程序的复杂度在一个合适的范围,说明我第一次作业的设计是比较合理的

(2)类规模

在第一次作业中我的每个类的规模都在100行以内,比较合理

(3)时序图

第二次作业

1.设计思路

第二次作业中,电梯仍然是一部电梯,只是算法上用ALS捎带算法代替了FAFS的傻瓜调度,所以整体的架构我还是沿用第一次作业,仍然以生产者消费者模式为主。三各类的功能和第一次作业大体相同,只不过因为ALS算法而添加了不同的方法,在这里不过多赘述, 整体结构图如下:

2.度量分析

(1)复杂度分析

Elevator.deal()函数的代码如下:

private void deal() throws InterruptedException {
sleep(30);
queueRequest = request.getQueueRequest(nowFloor);
if (first) {
request.getAnother(direction, nowFloor);
}
if (queueRequest.size() >= 1) {
int i = 0;
boolean isOpen = false;
while (i < queueRequest.size()) {
if ((queueRequest.get(i).getFromFloor() == nowFloor
&& request.getFlag(i) != 0)
|| (queueRequest.get(i).getToFloor() == nowFloor
&& request.getFlag(i) != 1)) {
isOpen = true;
break;
}
i++;
}
if (isOpen) {
open(nowFloor);
}
i = 0;
while (i < queueRequest.size()) {
if (queueRequest.get(i).getFromFloor() == nowFloor
&& request.getFlag(i) != 0) {
movePeople(nowFloor, queueRequest.get(i).getPersonId(),
"IN");
if (request.getFlag(i) == 1) {
request.setFlag(i, 0);
}
i++;
continue;
}
if (queueRequest.get(i).getToFloor() == nowFloor
&& request.getFlag(i) != 1) {
movePeople(nowFloor, queueRequest.get(i).getPersonId(),
"OUT");
queueRequest.remove(i);
request.removeFlag(i);
continue;
}
i++;
}
if (isOpen) {
sleep(timeOpenOrClose * 2);
close(nowFloor);
}
queueRequest = request.getQueueRequest(nowFloor);
if (first) {
request.getAnother(direction, nowFloor);
}
}
}

首先,从代码中很容易看出里面包含了很多的if分支以及if分支的嵌套,这导致了v(G)的数值很大;并且在函数内部的request.getQueueRequest()函数是对Request实例方法的调用,并且在其中调用了很多的本类中的方法,增加了模块与模块之间的调用,所以iv(G)的数值高;总体的导致整体复杂度ev(G)的提高。

Request.getAnother()的代码如下:

public synchronized void getAnother(int direction, int nowFloor) {
if (!queue.isEmpty() && queue.get(0) != null) {
int i = 0;
while (i < queue.size()) {
if (queue.get(i) != null) {
if ((queue.get(i).getToFloor() - queue.get(i)
.getFromFloor()) * direction > 0
&& (nowFloor - queue.get(i).getFromFloor())
* (nowFloor - queue.get(i).getToFloor()) >= 0) {
queueRequest.add(queue.get(i));
flag.add(-1);
queue.remove(i);
continue;
}
}
i++;
}
}
}

从中可以看车里面的if分支的条件判断十分复杂,并且存在嵌套,这大大提高了程序调试的难度,我在第二次作业中出现的BUG也正是因为这个方法中的条件判断出现了问题。

Request.getQueueRequest()方法的代码如下:

public synchronized ArrayList<
PersonRequest> getQueueRequest(int nowFloor) {
while (queue.isEmpty() && queueRequest.isEmpty()) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (queueRequest.isEmpty()) {
queueRequest.add(queue.get(0));
flag.add(1);
if (queue.get(0) != null) {
queue.remove(0);
}
}
if (queue.size() >= 1 && queue.get(0) != null) {
for (int i = 0; i < queue.size(); i++) {
if (queue.get(i) != null) {
if ((queueRequest.get(0).getToFloor() - nowFloor)
* (queue.get(i).getToFloor() - nowFloor) >= 0) {
queueRequest.add(queue.get(i));
flag.add(1);
}
}
}
for (int i = 1; i < queueRequest.size(); i++) {
queue.remove(queueRequest.get(i));
}
}
return queueRequest;
}

从代码中可以看到,在这个方法中使用了Elevator类中的成员变量nowFloor,并且其中使用了大量的ArrayList类型的成员变量,这大大提高了这个方法的耦合度,导致了Elevator和Request两个类之间的依赖关系十分显著。此外,其中大量的if分支嵌套也大大提高了复杂度。

(2)类规模

从图中可以看出只有*ELevator**类的行数高于100,我觉得其中的原因主要是由于第二次作业是在第一次作业的架构上进行的功能的添加。这导致了Elevator类中由于捎带增加了很多方法,以及每个方法的代码量。

(3)时序图

第三次作业

1.设计思路

在第三次作业中,要求三部电梯进行调度,并且不同的电梯的可达楼层不一样,这不仅导致了多线程的线程安全问题,也导致了请求的分配问题。因为一个人的请求有可能需要通过换乘来处理。我的程序主要保证的是正确性,所以牺牲了很大一部分的性能。我解决这种情况的方法是,将这种请求分割为两个请求,以1层为界,因为所有的电梯都可以到达1层。

并且在整体的结构上,我这次采用的是课件上的Worker-Thread模式,相当于对之前的作业进行了重构。

  • Main类是主类
  • Request类是请求类,是对提供的PersonRequest类型的请求的一个封装,因为考虑到要对请求的起始楼层进行修改
  • ClientThread类是输入类,负责将接收到的请求存储到Channel的请求队列中
  • Channel类是整体的调度器,用来将请求跟据不同的情况分配到不同的电梯中,并且在这次作业中,我采用请求队列和调度器二合为一的方式
  • WorkerThread类相当于前两次作业当中的电梯类

    -整体程序的架构图如下:

2.度量分析

(1)复杂度分析

由于第三次作业中的Channel.getAnother()Channel.takeRequest()WorkerThread.deal()三个方法和第二次作业中的基本相同,故复杂度不在这里重复分析。

WorkerThread.moveElevator()方法的代码如下:

    private void moveElevator(int floor, boolean doDeal)
throws InterruptedException {
while (nowFloor != floor) {
try {
if (nowFloor < floor) {
int temp = nowFloor;
for (int i = 1; i <= stopFloor.get(stopFloor.
indexOf(temp) + 1) - temp; i++) {
if (nowFloor == -1) {
nowFloor = 1;
i++;
} else {
nowFloor = temp + i;
}
sleep(moveTime);
arrive(nowFloor);
}
} else {
int temp = nowFloor;
for (int i = 1; i <= temp - stopFloor.
get(stopFloor.indexOf(temp) - 1); i++) {
if (nowFloor == 1) {
nowFloor = -1;
i++;
} else {
nowFloor = temp - i;
}
sleep(moveTime);
arrive(nowFloor);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
if (doDeal && nowFloor != floor) {
deal();
if (requestQueue.isEmpty()) {
break;
}
}
}
deal();
}

从中可以很容易的看出导致ev(G)和v(G)两个复杂度数值大的主要原因就是内部的if分支的嵌套问题。

WorkerThread.run()

 public void run() {
while (true) {
try {
sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (requestQueue.size() < maxNum) {
channel.takeRequest(requestQueue,
this.getName(), nowFloor, flag, maxNum);
}
if (requestQueue.size() == 1 && requestQueue.get(0) == null) {
break;
}
if (requestQueue.isEmpty()) {
continue;
}
try {
first = true;
direction = requestQueue.get(0).getFromFloor() - nowFloor;
logger.debug(direction);
moveElevator(requestQueue.get(0).getFromFloor(), false);
first = false;
Request another;
while ((another = dealFlag()) != null) {
direction = another.getToFloor() - nowFloor;
moveElevator(another.getToFloor(), true);
}
direction = requestQueue.get(0).getToFloor();
moveElevator(requestQueue.get(0).getToFloor(), true);
while (!requestQueue.isEmpty() && requestQueue.get(0) != null) {
if (flag.get(0) == 1) {
direction = requestQueue.get(0).getFromFloor();
moveElevator(requestQueue.get(0).getFromFloor(), true);
} else {
direction = requestQueue.get(0).getFromFloor();
moveElevator(requestQueue.get(0).getToFloor(), true);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

与前面的moveELevator()相比,除了因为if分支导致的结构复杂以外,这个函数中由于调用了Channel类的实例channel的方法,导致了两个类之间的调用关系复杂,依赖程度高,提高了程序的耦合度,而且作为一个线程的run()方法,这样的高复杂度很容易导致线程安全的问题

(2)类规模

(3)时序图

程序BUG的分析

在这三次作业的强测和互测阶段,我一共只遇到了一种BUG。这个BUG体现在输出中就是一个人还没有进电梯就从电梯里面出来了。我遇到这个BUG是在第二次电梯作业的强测阶段,一共有4个测试用例测出了我这个BUG,导致我自己差一点没进强测。

经过分析,我发现这个BUG出现的原因是在捎带请求的处理上,出问题的地方是Request类中:

public synchronized void getAnother(int direction, int nowFloor) {
if (!queue.isEmpty() && queue.get(0) != null) {
int i = 0;
while (i < queue.size()) {
if (queue.get(i) != null) {
if ((queue.get(i).getToFloor() -
queue.get(i).getFromFloor()) * direction >= 0
&& (queue.get(i).getFromFloor() - nowFloor) /***/
* direction >= 0 /***/
&& (queue.get(i).getToFloor() - nowFloor) /***/
* direction >= 0 /***/
&& direction != 0) { /***/
requestQueue.add(queue.get(i));
flag.add(-1);
queue.remove(i);
continue;
}
}
i++;
}
}
}

出问题的部分就是我在代码中标注出来的。这一部分的条件一开始我没有加上。我处理捎带请求的思路是,我的电梯刚开始向着主请求的起始楼层运动,在这个过程中,如果有一个请求的运行方向和我当前电梯运行方向一样,我就捎带。但是,我最初没有加上一个判断条件:这个捎带请求的起始楼层必须是还未到达。这就导致了我实际上在队列中存储了一个请求作为捎带请求,但是没有输出这个人进入电梯的消息,最终导致了BUG的产生。

互测

自动评测

在这三次的互测过程中,我采用的都是自动化评测的方式。

我的测评机主要分为一下几个部分:

  • 数据生成器
  • 定时输入数据
  • 结果检查

数据生成器

  • N:请求总数,通过randint声称
  • personId:人员Id,从0开始递增
  • Floor:一个列表,里面存储着电梯能够到达的楼层
  • Time:时间,起始值为0.0
  • TimeAdd:一个列表,Time的增长幅度,初值为[0.0, 0.1]

每一次循环,使用随机数来选择0.0或0.1作为时间的增长

用随机数来选择起始楼层和终 止楼层,如果选择的楼层一样则重新选择直到不同为止

将请求输出到文件中

定时输入数据(JAVA)

  • DealClass
  • InputClass
  • Instruction
  • MainClass
  • OutputClass
InputClass

返回一个String[]数组,内部保存着所有的输入

Instruction
  • 指令类
  • 内部有time(double)和ins(String)两个属性
  • time属性是请求的时间
  • Ins属性是请求
  • 构造方法
Public Instruction(double time, String ins) {
this.time = time;
this.ins = ins;
}
DealClass
  • 解析输入的请求
  • 通过正则表达式提取请求中的时间和请求本身
  • 使用instruction的构造方法
  • 返回一个Instruction[]数组
OutputClass
  • 输出线程
  • 当前时间now初始为0
  • 对于每一个请求,通过sleep的方式控制输出的前后
Thread.sleep((long)(time * (ins[i].getTime() - now)));
Now = instruction[i].getTime();

结果检查(python)

  • 初始化一个trueRequest字典和outRequest字典
  • trueRequest通过解析输入文件,得到每个人的真正请求
  • outRequest通过解析输出文件,得到每个人在电梯运行之后实际执行的请求
  • PersonId作为键值,[fromFloor, toFloor]作为元素,通过对比字典的每一项,如果相同就说明结果正确

Batch命令

javac -cp elevator-input-hw3-1.4-jar-with-dependencies.jar;timable-output-1.1-raw-jar-with-dependencies.jar src/*.java -d src/class
javac -cp elevator-input-hw3-1.4-jar-with-dependencies.jar;timable-output-1.1-raw-jar-with-dependencies.jar TestClass/src/*.java -d TestClass/class
set /a a = 0
:start
echo %a%
data.py
type in.txt |java -classpath TestClass/class MainClass |java -classpath elevator-input-hw3-1.4-jar-with-dependencies.jar;timable-output-1.1-raw-jar-with-dependencies.jar;src/class MainClass > dzh.txt
check.py
if %ERRORLEVEL% == 0 (
color 47
goto end
) else (
color 27
set /a a += 1
goto start
)
:end

结果展示

有效性

与第一次作业不同,我认为在本次评测过程中,自动化评测是十分必要的。在第一次作业里面,如果你不会自动评测,通过手动构造样例最起码是能够评测的,但是这次作业如果没有自动化的方式,光定时输入一点就很难做到。要想能够模拟评测的机的定时输入方式,必须要采用自动化评测的方式才够准确。如果是单纯的靠手输入,是难以保障准确性的。

但是呢,我认为线程安全方面的BUG,阅读代码要比自动评测更有效。因为通过代码结构的分析,你就能找到这个程序里面哪些资源是互斥的,哪些线程之间需要同步关系,一旦你找到了其中的BUG,那么只需要简单的构造一组样例就可以hack别人。

总结

本次的三次电梯作业,让我对多线程有了一个从无到有的实践。相比第一次作业,我的程序的设计明显的有了提高,在这三次作业中,我使用过两个模式:生产者模式Worker-Thread模式,这使得我的程序的模块的分工分明,大大降低了BUG修复过程中的困难。并且,在电梯作业的设计过程中,我对请求队列究竟放在电梯内部还是放在调度器中思考了很久,最终选择了放在电梯中。我认为这样的思考是一个很好的锻炼过程。

并且在这三次作业中我尝试了自己制作对拍器,也对batch命令有了更深的认识。

但是,在这三次电梯作业中我都没有去尝试对电梯的调度进行优化,在研讨课上听了大佬们的LOOK算法,觉得这一部分的缺漏是一个很大的需要补的地方。并且,对于多线程里面锁的使用,我都是直接将方法上了锁,而没有采用锁代码块或者采用阻塞队列这样更高效率并且更高端的操作,我认为这些都是在以后需要提高的。

OO——电梯作业总结的更多相关文章

  1. OO电梯作业总结

    (一)第五次作业 一.设计思路 生产消费者模型,输入接口是producer,调度器是tray,电梯是customer.由于只有一架电梯,所以生产消费模型满足以下条件: 一个生产者,一个消费者 托盘不为 ...

  2. OO第三次电梯作业优化

    目录 第三次电梯作业个人优化 前言 优化思路 一.调度器 二.电梯 第三次电梯作业个人优化 前言 由于个人能力有限,第二次电梯作业只能完成正确性设计,没能进行优化,也因此损失了强测分数,于是第三次电梯 ...

  3. OO第二单元——电梯作业总结

    前言 本单元作业主要以设计电梯来实现多线程编程.本章主要学习了如何使用多线程以及如何确保多线程安全,从电梯的调度策略中学会了如何简单地使用synchronized锁来控制线程安全. 首先,明确锁的两个 ...

  4. 从入门到不放弃——OO第一次作业总结

    写在最前面: 我是一个这学期之前从未接触过java的小白,对面向对象的理解可能也只是停留在大一python讲过几节课的面向对象.幸运的是,可能由于前三次作业难度还是较低,并未给我造成太大的困难,接下来 ...

  5. OO第二单元电梯作业总结

    目录 目录一.第一次作业分析设计策略基于度量分析程序结构二.第二次作业分析设计策略基于度量分析程序结构三.第三次作业分析设计策略基于度量分析程序结构四.分析自己程序的bug五.发现别人程序bug所采用 ...

  6. OO第一次作业总结

    OO第一次学习总结 1.第一次作业:多项式加法 从未接触过java的我,在从输入输出开始学了几天后,按照C语言的思路,写出了一个与面向过程极其接近的程序. 在这个程序中,存在两个类:一个是Comput ...

  7. 学会拒绝,是一种智慧——OO电梯章节优化框架的思考

    在本章的三次作业里,每次作业我都有一个主题,分别是:托盘型共享数据.单步电梯运行优化.多部电梯运行优化,因而电梯优化实际是第二.三次作业.虽然后两次作业从性能分上看做得还不错,但阅读其他大佬博客,我深 ...

  8. OO电梯系列总结与反思

    目录 前言 HW5 度量分析 UML类图与协作图 bug分析 HW6 度量分析 UML类图与协作图 bug分析 HW7 度量分析 UML类图与协作图 bug分析 SOLID原则 感想 前言 紧张刺激的 ...

  9. oo第一次作业

    前言: 这是一篇面向对象作业总结,作业内容是对多项式进行求导,一共有三个阶段,具体要求不详述,第一阶段只要求’+’连接coeff*x^pow的形式,第二次支持*连接的幂函数及三角函数,第三次则需要支持 ...

随机推荐

  1. SD从零开始45-46

    [原创] SD从零开始45 运输流程的控制 运输业务场景的例子Examples 一个公司可使用不同的运输业务场景,通过不同的处理类型或者运输方式来刻画: 要模型化这些不同的装运,你可以在配置中定义装运 ...

  2. Python Django框架笔记(三):django工作方式简单说明和创建用户界面

    (一)  说明 简单说明下django的工作方式,并举2个例子. (二)  Django工作方式 假定我们有下面这些文件 ,这里在前2篇的基础上增加了 templates目录(存放html文件) 和s ...

  3. Bootstrap源码分析系列之整体架构

    作为一名合格的前端工程师,你肯定听说过Bootstarp框架.确实可以说Bootstrap框架是最流行的前端框架之一.可是也有人说Bootstrap是给后端和前端小白用的,我认为只要学习它能给我们前端 ...

  4. python终端总是无法删除字符

    yum install readline-devel

  5. 什么是 Azure 中的虚拟机规模集?

    虚拟机规模集是一种 Azure 计算资源,可用于部署和管理一组相同的 VM. 由于所有 VM 的配置都相同,因此无需对 VM 进行任何预先配置. 这样就可以更方便地构建面向大型计算.大数据.容器化工作 ...

  6. 在IE中,JS方法名和input的name重名时,调用该方法无效

    在IE中,JS方法名和input的name重名时,调用该方法无效.提示:网页错误详细信息 用户代理: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1 ...

  7. Eclipse+Weblogic 12开发简单的Enterprise Application

    学到EJB方面的内容,遇到了很多问题,翻阅了无数遍Java EE和Weblogic的官方文档,在google上进行了无数次搜索都没有答案,可能我要找的答案太冷门.这一切都起源于Java EE官方文档里 ...

  8. js经典应用

    一.js字符串转数字: 1.parseInt()和parseFloat()两个转换函数: 2.强制类型转换,Number(value)——把给定的值转换成数字(可以是整数或浮点数): 3.利用js变量 ...

  9. cisco查看机框 板卡 电源 SN 风扇环境运行状态和一些常用命令 巡检命令

    查看设备运行环境及状态 show environment 查看设备环境show environment temperature --查设备温度 show environment fans --查看设备 ...

  10. 通过yum源在centOS7安装mysql8

    1.去官网下载rpm文件,该文件专门用于yum安装方式: 到官网https://www.mysql.com/downloads/下载社区版Community(针对个人),如下图: 然后拉到最下面,我下 ...