- 分析和总结自己三次作业的设计策略
- 第一次作业
- 第二次作业
- 第三次作业
- 分析和总结自己第三次作业架构设计的可扩展性
- 功能设计与性能设计的平衡方面
- 设计原则分析
- 基于度量来分析自己的程序结构
- 第一次作业
- 第二次作业
- 第三次作业
- 分析自己程序的bug
- 分析未通过的公测用例和被互测发现的bug
- 分析自己发现别人程序bug所采用的策略
- 列出自己所采取的测试策略及有效性
- 分析自己采用了什么策略来发现线程安全相关的问题
- 分析本单元的测试策略与第一单元测试策略的差异之处
- 心得体会
- 线程安全
- 设计原则
分析和总结自己三次作业的设计策略
第一次作业
-
多线程的协同:只有主线程、输入处理线程、调度器线程、电梯线程。由于只有一部电梯,所以线程的数量是固定的。我的设计是在主线程里启动输入处理线程和调度器线程,由调度器启动电梯线程。在主类里等待输入处理线程join,join之后设置结束信号为true,直接关闭调度器,电梯收到结束信号并且工作完后自己结束。
-
同步控制:电梯、调度器共享一个结束信号的锁,收到结束信号并且工作完后自己结束。采用了
java
自带的线程安全容器ArrayBlockingQueue
省了很多事,直接实现消费者生产者模型(输入处理线程与调度器线程之间)。
第二次作业
-
多线程的协同:还是主线程、输入处理线程、调度器线程、电梯线程。不过由于有x部电梯,所以在调度器里用
ArrayList
管理了x个电梯线程。我的设计是在主线程里启动输入处理线程和调度器线程,先锁住调度器,等待输入处理线程getElevatorNum()
,等来了x = ElevatorNum
就唤醒调度器,启动x部电梯线程。之后的协同就和第一次作业一样。 -
同步控制:输入处理线程和调度器线程共享一个
inputLock
,这个lock负责同步getElevatorNum()
。x个电梯、调度器共享一个结束信号的锁,收到结束信号并且工作完后自己结束。调度器负责与输入处理线程组成消费者生产者,拿到请求后,根据调度的策略给每个电梯分派任务。
第三次作业
-
多线程的协同:还是主线程、输入处理线程、调度器线程、电梯线程。不过多了动态添加电梯的请求。我在这里的处理是把2种请求用统一的队列管理,新建电梯请求也可以利用永不出现的0楼,将from设为0,id和to分别储存电梯id与类型,调度线程接收到该请求后就根据信息创建电梯。之后的协同就和第一次作业一样。
-
同步控制:x个电梯、调度器共享一个结束信号的锁,收到结束信号并且工作完后自己结束。调度器负责与输入处理线程组成消费者生产者,拿到请求后,根据调度的策略给每个电梯分派任务。由于有换乘,所有的电梯线程都共享了调度器(参考了
worker-thread
的思想),某电梯到了换乘的楼层就用调度器把换乘者的请求派发给别的电梯。
分析和总结自己第三次作业架构设计的可扩展性
功能设计与性能设计的平衡方面
功能设计:
- 第三次作业的功能是多部多线程可捎带调度电梯,我也是从第二次作业发展到第三次的,仅仅是增加了换乘的功能以及动态增加电梯的功能。所以我只是增加了一个电梯地图类来管理电梯的直达逻辑。
- 由于每个类的职责分工都很明确,再来新的需求原来的架构也不会有很大改变,所以我觉得功能的扩展性应该还行。
性能设计:
- 电梯的运行策略沿用前两次作业。每一部电梯自己都是look策略来决定移动的方向和目标楼层,尽量让电梯能够在大范围移动。
- 调度器的策略是第三次独特的,我采取的是:对于一个人的请求,先看他能否直达,如果能直达,就用这个类型的电梯去接,如果不能直达,就从其他电梯类型里随便选一个类型的电梯。先确定好类型,在从这个类型里随机选一部电梯去接。
- 换乘的策略是固定只能在15和1楼换乘,某电梯到了换乘的楼层就用调度器把换乘者的请求派发给别的电梯。
- 看起来策略是很简单的,但是在随机情况来看,还是能够平衡每个乘客等待时间与电梯运行时间都保持较短。
设计原则分析
Project 7 | |
---|---|
SRP-单一功能 | 将人、电梯、电梯地图各自分为一个单独对象,符合SRP的设计模式 |
OCP-开闭原则 | 留出了尽可能多的扩展性,以便后续使用,符合OCP |
LSP-里式替换 | 未使用继承 |
ISP-接口隔离 | 所实现接口仅有Runnable,符合ISP |
DIP-依赖反转 | 不存在对任何一个类的抽象,全部依赖实体,未实现DIP |
基于度量来分析自己的程序结构
第一次作业
方法的度量如下:
可以看出,电梯类里面方法的复杂度比较高,这是由于我的look
运行算法都写在电梯类里了。
优点:写了Scheduler
类,虽然第一次作业没有什么用,但是留够了可扩展性
缺点:Elevator
类有点上帝类的感觉,把所有的事情都干了,有些臃肿。
第二次作业
可以看出,电梯类里面方法的复杂度依然比较高,我把所有要打印的方法都转移到电梯类了,方便统一管理。
类图如下:
优点:从第一次作业扩展来的,架构没有发生变化,说明职责划分得比较合理。
缺点:
- 第二次作业把
Person
类的一部分职责转移给了Elevator
,说明第一次作业考虑的不周全。 - 对于输出,没有用锁包装,留下隐患。
UML图如下:
(相对于第一次作业的改变,用蓝色线条表示)
第三次作业
方法的度量如下:
可以看出,方法的平均复杂度比第二次下降了,这是因为我简化了调度算法,很多都是简单的随机分配,所以方法复杂度降低了。但是测试结果表示,这种随机分配性能不比我之前按向量排序分配差。
优点:从第二次作业扩展来的,仅仅增加了ElevatorMap
类,其余架构没有改动。
缺点:采用的是预分配的调度策略,有一定的时间浪费。
UML图如下:
(相对于第二次作业的改变,用绿色线条表示)
分析自己程序的bug
分析未通过的公测用例和被互测发现的bug
(1)强测
在第二次强测中,我有2个测试点RTLE
了。
令我沮丧的是,我用自己的定时投放的评测机按照强测的数据进行了本地测试,结果反复测了200遍,这两个点的错误还是没有复现。
我只好一个字都没改,交上去bug修复,发现这两个点又直接通过了,很迷惑。后来听说是本地的测试环境和oo课程网站的测试环境并不相同,可能有些波动。
我在课下尝试了多种方法(白盒黑盒)测试自己的程序,相信自己的程序没有bug。
于是我还是安心地写第三次作业,不过我开始非常注意线程安全的问题,还用锁包装了output
。
(2)互测
三次互测都没有被发现bug。
分析自己发现别人程序bug所采用的策略
列出自己所采取的测试策略及有效性
-
随机测试
-
随机生成请求,检测程序是否输出异常,靠的是大样本测试。
-
我3次作业通过这个测略,均发现了其他同学的bug
-
-
手动测试
- 自动生成的测试数据没有针对性,有时还是需要对症下药的。
shell
脚本还是与自动评测机差不多的思路,只是把自动生成测试数据,改为手动输入罢了。 - 第二次作业的时候室友分享了一个专门针对死锁的样例,我拿来手动一测,果然发现2位同学的程序中招了。
- 自动生成的测试数据没有针对性,有时还是需要对症下药的。
分析自己采用了什么策略来发现线程安全相关的问题
-
在动手写代码之前,先在草稿纸上分析自己要写哪些类,会有哪些是线程,之间有什么关系。
-
向想象中的小黄鸭解释自己的代码是如何运行的,可以发现自己的死锁bug(
-
JProfiler
点CPU Views,选择Thread status为All status可以看到所有方法占用的CPU时间,它显示的是调用树,看哪个方法CPU时间太长来检测自己是否暴力轮询了 -
JProfiler
点Threads,弹窗里显示的是现在处于Waiting和Runnable状态的线程(最好在代码里setname给线程设个名字,不然看不出是哪个线程了)这样检查线程的等待逻辑是否正确
分析本单元的测试策略与第一单元测试策略的差异之处
- 第一单元没有线程安全的问题,而本单元的重点就是多线程,所以本单元的测试相比而言要更注重线程安全的测试。
- 第一单元任何错误都可以复现,但是本单元由于线程调度的不确定性,很多错误没法复现。要好好从源头解决问题,深入分析自己代码的结构来查找错误。
- 第一单元一般都是黑盒测试就够了。而第二单元黑盒测试是很难发现一些隐藏的线程逻辑的bug,所以我采用了白盒测试+黑盒测试,并且使用了
JUnit
单元测试。
心得体会
线程安全
- 设计>写代码。好的设计可以真正解决线程安全,先构思好再动手。
- 多利用学习到的线程模式
设计原则
- 在第四次课上实验的时候,看到了
worker-thread
模式的代码,觉得很好,有面向接口编程的味道。可惜我前两次的作业的时候都不知道worker-thread
模式,就没有采用,第三次改也是不可能的了。所以最好多学一些好的设计模式,这些模式自然就符合一些SOLID
设计原则。 SOLID
设计原则里面,自己最符合的就是开闭原则和单一职责原则,设计的时候我留出了尽可能多的扩展性,以便后续使用。每个类职责划分清晰,使得他们高内聚低耦合。