OO第二单元总结——电梯调度问题
一、设计策略。
在三次作业中,多线程程序的实现分以下几个步骤:
1. 主线程Main类的创建多个线程。
2. 共享对象的synchronized锁保证线程之间的互斥访问。
3. 采用notifyAll和wait的方式实现同步控制,第三次作业中少部分采用sleep(10)的轮询机制(本希望能去掉,但时间不足)。
4. 结束指令NULL的异步传递(传递到共享对象中,由各个线程读取)结束线程。
第一次作业的结构比较好,耦合度低,使得第二次作业轻松摸鱼,修改很少,但没有为下一次作业修改架构。后果是第三次作业的时间不足,第二次作业的架构又不适应第三次作业的要求,导致强测炸点。
二、基于度量和SOLID原则分析程序结构
SOLID简介
S.O.L.I.D是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写。
SRP | The Single Responsibility Principle | 单一责任原则 |
OCP | The Open Closed Principle | 开放封闭原则 |
LSP | The Liskov Substitution Principle | 里氏替换原则 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 |
ISP | The Interface Segregation Principle | 接口分离原则 |
作业一、单人电梯
1. 统计信息图
2. 复杂度分析
基本复杂度(Essential Complexity (ev(G))、模块设计复杂度(Module Design Complexity (iv(G)))、Cyclomatic Complexity (v(G))圈复杂度
OCavg为平均循环复杂度;WMC为总循环复杂度
3. 结构信息图
可见,由于逻辑简单,本次作业的复杂度和耦合度都不高。
由UML类图可以看出,主线程是Main类(启动子线程后立即结束),创建了两个子线程Inputhandler和Elevator,两者的共享对象是Schedule类。
4. SOLID分析
单一责任原则:基本实现
开放封闭原则:基本实现
里氏替换原则:基本实现
依赖倒置原则:基本实现
接口分离原则:基本实现
作业二、单部捎带电梯
1. 统计信息图
2. 复杂度分析
基本复杂度(Essential Complexity (ev(G))、模块设计复杂度(Module Design Complexity (iv(G)))、Cyclomatic Complexity (v(G))圈复杂度。
OCavg为平均循环复杂度;WMC为总循环复杂度
3. 结构信息图
可见,由于逻辑简单,本次作业的复杂度和耦合度都不高。
由UML类图可以看出,主线程是Main类(启动子线程后立即结束),创建了两个子线程Inputhandler和Elevator,两者的共享对象是Schedule类。
本次作业结构继承了上一次作业,仅仅在Elevator和Scheduler类中加入了3个方法,说明上一次作业的结构设计合理,可以适应本次作业的修改。但同时这次作业没有为下一次作业而修改结构,轻松摸鱼,为第三次作业的悲剧埋下了伏笔。
4. SOLID分析
单一责任原则:Elevator中不应该出现sort方法,这是属于Schedule的工作,应该放到Schedule中。
Schedule中的search方法过于臃肿,应该拆分。
开放封闭原则:可扩展性低,当多个Elevator共享一个Schedule时,会出现很多问题。
里氏替换原则:基本实现
依赖倒置原则:抽象的不是很好,在sort中考虑了过于细节的问题(比如乘客是否在电梯内,方向是否一致)。
接口分离原则:基本实现
作业三、多部捎带电梯
1. 统计信息图
2. 复杂度分析
基本复杂度(Essential Complexity (ev(G))、模块设计复杂度(Module Design Complexity (iv(G)))、Cyclomatic Complexity (v(G))圈复杂度
OCavg为平均循环复杂度;WMC为总循环复杂度
3. 结构信息图
可以发现,本次作业不论是复杂度还是耦合度都很高,这给测试和改BUG带来了很大的困难。
由UML类图可以看出,主线程是Main类(启动子线程后立即结束),创建了四个子线程InputHandler和三个Elevator,Inputhandler和Elevator之间的共享对象是Schedule类;Elevator和Schedule之间的共享对象是ElevSchedule,Elevator间共享对象是Output和Empty。
4. SOLID分析
单一责任原则:Elevator中不应该出现sort方法,这是属于Schedule的工作,应该放到Schedule中。
Schedule中的search方法过于臃肿,应该拆分。
为了判断NULL信号,用了Empty类以及其中的方法、ElevSchedule中的isEmpty方法和Num方法。
比如下图的代码(截取自第三次作业)就是一个很好的反面材料(if语句嵌套过多,与多个类有联系(耦合度高),存在轮询):
1 if (order.get(0).get(1) == 0) {
2 if (order.size() == 1) {
3 // System.out.println(name + order);
4 order.remove(0);
5 boolean end = false;
6 empty.setEmpty(name);
7 while (true) {
8 if (empty.isEmpty() & (elevScheduler.Num() == 0)) {
9 // System.out.println(name + ": end==========");
10 if (!closeDoor) {
11 close();
12 }
13 end = true;
14 break;
15 }
16 if (elevScheduler.Num() != 0) {
17 ArrayList<ArrayList<Integer>> integer
18 = elevScheduler.pop();
19 if (integer.get(0).get(1) == 0) {
20 sleep(100);
21 continue;
22 }
23 order.addAll(integer);
24 empty.setFull(name);
25 // System.out.println("addBack" + order);
26 break;
27 }
28 // System.out.println(name + " wait...");
29 sleep(100);
30 }
31 if (end) {
32 break;
33 }
34 } else {
35 order.remove(0);
36 }
37 }
开放封闭原则:可扩展性低,如果再加一个电梯,几乎不能运行,硬编码过多。
如下图:
synchronized void add(ArrayList<Integer> list) {
// System.out.print("list");
// System.out.println(list);
if ((list.get(1) == 0)) {
list.add(0);
elevSchedulerA.add(list);
elevSchedulerB.add(list);
elevSchedulerC.add(list);
inputEnd = true;
} else if (floC.contains(list.get(1)) && floC.contains(list.get(2))) {
elevSchedulerC.add(list);
} else if (floA.contains(list.get(1)) && floA.contains(list.get(2))) {
elevSchedulerA.add(list);
} else if (floB.contains(list.get(1)) && floB.contains(list.get(2))) {
if (floC.contains(list.get(1)) && floC.contains(list.get(2)) &&
(!elevSchedulerC.isFull() || elevSchedulerC.Num() < 5)
&& (elevSchedulerB.isFull() || elevSchedulerB.Num() > 5)) {
elevSchedulerC.add(list);
} else {
elevSchedulerB.add(list);
}
} else {
list.add(1);
if (floA.contains(list.get(1))) {
if (floB.contains(list.get(2))) {
elevSchedulerA.add(list);
} else if (floC.contains(list.get(2))) {
elevSchedulerA.add(list);
}
} else if (floB.contains(list.get(1))) {
if (floA.contains(list.get(2))) {
elevSchedulerB.add(list);
} else if (floC.contains(list.get(2))) {
elevSchedulerB.add(list);
}
} else if (floC.contains(list.get(1))) {
if (floA.contains(list.get(2))) {
elevSchedulerC.add(list);
} else if (floB.contains(list.get(2))) {
elevSchedulerC.add(list);
}
}
}
}
里氏替换原则:基本实现
依赖倒置原则:抽象的不好,高层模块Schedule为了满足底层模块Elevator的需求,将高层模块的逻辑修改,加入了addBack方法,将指令长度从4扩展到5。
接口分离原则:基本实现
三、自己程序BUG分析
前两次作业未发现BUG,第三次作业中出现了一个BUG(中了三个强侧点),具体分析如下:
当三个elevScheduler中的队列都为空,且A、B、C电梯为空时,且最后一个请求是拆分请求(一部电梯不能到达),会向最后一部运行电梯的elevScheduler发送全零信号,导致给电梯线程在没有输入NULL时提前结束。出现TLE或者WA(因某电梯线程结束乘客没有送到指定位置)
从程序上来说,是由于读入NULL的设计失误导致:
我为了保证遇到NULL且需要换乘的乘客在第一个电梯里时,换乘的第二个电梯不会停止,任意电梯停止前,会查看其他电梯的情况,若有正在运行的电梯,则wait并删掉NULL信号(全零信号)。当该乘客换乘时,再向Scheduler中模拟写入一个NULL信号(全零信号)。那么,当真正的NULL信号没有出现时,模拟写入一个NULL信号会导致电梯线程中止(没有判断InputHandler线程的运行情况)。
BUG出现的原因:
测评机出世后,爱不释手,终于在跑了400+组后出现一个BUG,于是开开心心的改完就提交了,却忘了检验新的逻辑正确性,恰巧我们的评测机测不出新的BUG,于是BUG就顺理成章地出现在了强测中。。。
获奖感言:
不可太信任评测机,有时手动生成的数据更靠谱(比如“分布式集中投放”,而不是单独一个点的集中投放指令),有了评测机就懒了。
改BUG(尤其是逻辑BUG)时,一定要时间充沛、精力旺盛时,仔细想清楚改完BUG后的逻辑,有没有新增的漏洞。改完BUG不能高兴,切实提高正确率才能偷偷乐一下。
反思:
其实加入判断InputHandler线程的运行情况只是一个临时方案,最好的解决方法是,将判断结束信号单独写一个共享对象,保证所有线程正确关闭,也可以去掉复杂的判断方法(isEmpty等)和多余的类(Empty类),减少程序复杂度和耦合度。
四、他人程序BUG分析
前两次作业的易错点不多,均没有发现BUG(其实是不知道怎么测,所以都没有提交测试用例)。第三次作业决定搞一番事情,于是和几个同学一起搭了评测机。一亮相,就WA声一片,于是回头改BUG。。。
评测机的故事:
在第二次互测中无事可做地划水后,感觉需要什么来提高效率,增加互测中的hack数。正巧,jly巨佬招人做对拍器,于是欣然起行,举手报名,开启了罪恶的一生。 However,入党发展对象答辩悄然降临在周日晚上,我只好先去准备答辩,偷空写OO & OS。显然偷空是不够的,直到周日晚上任未写出能跑的程序。周一上午终于提交了第一版,AC:WA = 1:9,心灰意冷的我顺利地在下午的OS考试中挂掉,又理直气壮的“休息“了一个晚上。到了11点,不知哪来的自信,可能是秉着磨刀不误砍柴工的真理,开始写评测机??终于,2点的时候,带着残缺的评测机入睡了。(就是太懒了,不想写) 周二中午,老天开眼,解决了大部分的BUG,通过中测,评测机(检验输出部分)也顺利运行,和几位大佬的程序合并后,测评机成功跑了起来! 有了测评机后,可以同时开着8个测试程序,一个小时干掉了我手动一天的DEBUG量,评测姬nb!
看到这里读者应该可以料想到,一个人强测炸了三个点进入C组,恰巧手里拿着评测机,会做出什么样的事情呢。
于是乎,一启动评测机,就是WA声一片,就放弃了阅读代码发现BUG的方法(其实不应该放弃,最起码看一看别人的思路),随机选取一些WA掉的测试点就提交了。最后的结果是26 / 70。大部分错误是以下几个方面:
1. 线程未停止
2. 有乘客未被接上就结束线程
3. 数组 / list越界
4. 线程停止时没有关门
本次结果可以看出,线程安全是个大问题,互斥和同步,什么时候停止都可能出现BUG。
五、心得体会
1. 框架设计很重要,第一次作业做的好使得第二次作业很轻松,但是第二次作业没有做,导致第三次作业的滑铁卢。
2. 评测机很重要,大大提高Debug效率,但是一定要结合自己的逻辑验证,防止评测机的片面性导致的BUG盲区。
3. 多线程停止时,最好建一个类或者单一变量,让它做众多停止信号和实际停止的桥梁,不要直接用一大堆信号做实际停止的标志。
4. 减少硬编码,灵活使用工厂模式、抽象工厂模式等方法。
5. 单一任务原则,不要试图在一个类或一个方法中干过多的事情。
6. 不要轻易改BUG,先想清楚逻辑上的思路,精准定位哪里出错了,怎么改最好。
希望多思考,多实践,多看大佬分享的思路,OO加油啊~
OO第二单元总结——电梯调度问题的更多相关文章
- OO第二单元多线程电梯总结
OO第二单元多线程电梯总结 第一次作业 设计思路 Input为输入线程,负责不断读取请求并将读到的请求放入调度器中. Dispatcher为调度器,是Input线程和Elevator线程的共享对象,采 ...
- OO第二单元多线程电梯总结分析
一.概述 这一部分的作业考察的关注点与上一次的作业有所不同,上一次的考察重点主要集中在输入输出的判定以及多态的考察上面,而这一次是让我们进行多线程程序的调度与开发.这次开发过程中最大的感受就是自己之前 ...
- OO第二单元总结——电梯
在电梯系列的作业中,笔者的整体架构几乎没有发生改变.现介绍如下,对于一个电梯系统,主要的工作步骤就是获取乘客请求.分派请求.执行请求.针对这样的工作模式,笔者设计了Elevator.Uselist两个 ...
- 电梯也能无为而治——oo第二单元作业总结
oo第二单元作业总结 一.设计策略与质量分析 第一次作业 设计策略 在第一次作业之前,我首先确定了生产者--消费者模式的大体架构,即由输入线程(可与主线程合并)充当生产者,电梯线程充当消费者,二者不直 ...
- OO第二单元——多线程(电梯)
OO第二单元--多线程(电梯) 综述 第二单元的三次联系作业都写电梯,要求逐步提高,对于多线程的掌握也进一步加深.本次作业全部都给出了输入输出文件,也就避免了正则表达式判断输入输出是否合法的问题. 第 ...
- 2020北航OO第二单元总结
2020北航OO第二单元总结 前言 本单元考察基于多线程的电梯调度问题,成功让我从一个多线程小白到了基本掌握了使用锁来控制线程安全的能力,收获颇多(充分体验了迷茫地de一个又一个死锁bug的痛苦). ...
- oo第二单元作业总结
oo第二单元博客总结 在第一单元求导结束后,迎来了第二单元的多线程电梯的问题,在本单元前两次作业中个人主要应用两个线程,采用“生产者-消费者”模式和共享数据变量的方式解决问题.在第三次作业中加入多个电 ...
- OO第二单元优化博客
OO第二单元优化博客 第五次作业没有性能分,但是,我在这一单元的宗旨就是写一个日常生活中 最常见的那种电梯,所以第五次我没有写傻瓜电梯,而是直接写了个\(look\),和第六次基本相同. 总计一下lo ...
- 【OO学习】OO第二单元作业总结
OO第二单元作业总结 在第二单元作业中,我们通过多线程的手段实现了电梯调度,前两次作业是单电梯调度,第三次作业是多电梯调度.这个单元中的性能分要求是完成所有请求的时间最短,因此在简单实现电梯调度的基础 ...
随机推荐
- 线程锁的机制Lock
java.util.concurrent.locks 接口Lock publci interface Lock Lock 实现提供了比使用synchronized方法和语句可获得的更加广泛的锁定操作, ...
- C#--二维码生成代码
需要用到ThoughtWorks.QRCode.dll文件 string str = Server.UrlDecode(Request.QueryString["id"]); QR ...
- chmod和chown命令具体使用方法
Linux下数字表示文件的操作权限(777,755,..) Linux下.查看某路径下用(ls -l)查看全部文件的具体属性列表时.会看到文件的操作权限.类似"drwxr-xr-x" ...
- SQL从头開始
SQL 分为两个部分:数据操作语言 (DML) 和 数据定义语言 (DDL) 查询和更新指令构成了 SQL 的 DML 部分: SELECT - 从数据库表中获取数据 UPDATE - 更新数据库表中 ...
- multiset多重集合容器
跟set集合容器相比,multiset多重集合容器也使用红黑树组织元素,仅仅是multiset多重集合容器同意将反复的元素键值插入.元素的搜索依旧具有对数级的算法时间复杂度,find和equal_ra ...
- 谷歌开源可视化工具Facets,将用于人+AI协作项目研究——无非就是一个用于特征工程探索的绘图工具集,pandas可以做的
见:http://www.infoq.com/cn/news/2017/07/goole-sight-facets-ai https://github.com/PAIR-code/facets/blo ...
- centos + nodejs + egg2.x 开发微信分享功能
本文章发到掘金上,请移步阅读: https://juejin.im/post/5cf10b02e51d45778f076ccd
- Java基础——选择排序、冒泡排序
1.选择排序 原理是直接从待排序数组里选择一个最小(或最大)的数字,每次都拿一个最小数字出来, 顺序放入新数组,直到全部拿完 代码演示: public class Test3 { public sta ...
- 取消overflow-scroll的滚动条
通常情况下设置完overflow:scroll之后,就会在页面中出现滚动条,下边的方法可以取消掉此滚动条: container为当前设置overflow:scroll的元素 1.使用以下CSS可以隐藏 ...
- 用endnote导入bib
首先一般时候需要把IEEE的style包导入. https://endnote.com/downloads/styles/ 具体方法可参考http://muchong.com/html/201006/ ...