目录
- 1. 设计策略
- 多线程间的协同
- 同步控制
- 2. 架构设计的可扩展性
- 可扩展性
- 功能设计和性能设计
- 设计原则SOLID检查
- SPR-Single Responsibility Principle
- OCP-Open Close Principle
- LSP-Liskov Substition Principle
- ISP-Interface Segregation Principle
- DIP-dependency Inversion Principle
- 3. 基于度量分析程序结构
- 第一次作业
- 第二次作业
- 第三次作业
- 4. 分析自己程序的bug
- 5. 分析别人bug所采用的策略
- 6. 心得体会
- 关于程序检测的心得
- 线程安全
- 设计原则
- 接下来要学习的
1. 设计策略
多线程间的协同
三次作业我采用的整体架构基本没有发生变化,都是 以Worker模式为基础的,如下图所示,也正是老师推荐的架构。其中全局调度器只负责将全局请求队列中的每一个请求分配到一个合适(也就是预期得到性能最好)的电梯局部请求队列。而电梯内部的局部调度器根据当前的电梯状态和局部队列产生一个Instruction,电梯则只需要根据当前的Instruction运行即可。因此做到了不同线程之间尽量少交互。
内含有两个producer-consumer模式
- 请求模拟器作为producer, 全局scheduler作为consumer, 全局队列作为tray
- scheduler作为producer, elevator作为consumer, 电梯局部队列作为tray
线程间的交互除了两个共享对象tray外还有: - scheduler和elevator共享电梯的状态
- scheduler和elevator共享全局队列(第三次作业要求换乘)
总体来说我多线程间的协同还是不错的,和老师推荐的架构基本一致。但是还有一些地方不足,这些不足基本都是和同步控制相关的,因此放在同步控制中分析。
同步控制
三次作业的同步控制我都是采用synchronized关键字。但是由于我在写作业时还没有以下概念:
- 锁加在何处:锁住共享对象
- 将所有共享对象都设计成线程安全类
我并没有清晰地认识到实际上我的程序中的共享对象应该是全局请求队列和局部请求队列,而我将scheduler当成了共享对象。因此在共享对象进行read-modify-write和check-then-act时都将scheduler作为锁。这样的话,程序扩展性差。
另外,调度器如何获取电梯的状态,我用的是:调度器拥有对电梯线程对象的引用,把电梯当成一般对象进行访问。这样并不是一个好的方案。因此此时电梯的状态也是一个共享对象,所以应该设置一个线程安全类的共享对象StatusBoard,电梯把自己的状态发布d到board, 调度器从中读取电梯状态。
2. 架构设计的可扩展性
可扩展性
本单元作业我的可扩展性还是比较好的。
因为各个模块间耦合度较低,职责明确。全局schduler就只负责分配请求到局部队列。局部scheulder(电梯内部)就只负责根据局部队列和电梯状态产生电梯可执行的指令。
功能设计和性能设计
本单元作业我对自己的功能设计还是很满意的,得益于良好的架构。
但是我三次作业性能方面的表现不尽人意。三次作业强测得分分别为94,98,93。其实我的设计中,将全局scheduler和局部scheduler中的调度策略算法都封装成了单独的模块(函数),照理来说很容易优化。没有取得一个好的结果完全是本人脑抽的结果。
我的优化目标不够直接,应该是直接以时间作为优化目标。而我却以自己指定的各种规则作为优化目标(上帝才知道我自己瞎制定的那些规则能不能最大化程序的性能呢)。现在反思,我在调度策略上应该采用如下架构。
- 全局调度器 : 全局调度器应该尽量保证负载均衡,我却将负载均衡放在了第四优先级。
- 局部调度器 : 原本的局部调度器我是死守look算法特别是在第三次作业中look算法相比于ssft比较吃亏。现在看来我应该以时间(性能)为目标合理选择。每来一个新的请求进入局部队列,都预估时间代价,采用贪婪算法,是转头去接新乘客还是等下一趟scan时再捎带。
- 换乘调度器 : 第三单元作业其实我设计的调度器非常简略。应该考虑不同电梯的速度来指定换成策略.
设计原则SOLID检查
SPR-Single Responsibility Principle
得益于良好的架构,我在这一点上做的不错。
OCP-Open Close Principle
开闭原则指的是对扩展开放,对修改封闭。具体来说,当需求变化时,不要去修改源代码,通过继承或者接口实现新的需求。
实话说,我在前两个单元中,都没有很好的做到开闭原则。每次都想偷懒,所以每次都在上一次的实现上直接修改源代码来实现新的需求。这在实际工程的迭代开发中非常不好,因为很多时候之前的源代码是其他开发者写的,直接修改别人代码,说不定就掉到哪个坑里了、
而OCP的具体实现就是LSP, ISP和DIP原则,因此接下来在这三个原则里具体分析应该怎么做。
LSP-Liskov Substition Principle
本次电梯可以划分为以下层次结构,有一个实现了up,down,open,close,executeInstruction等具体方法的抽象类elevator。
直接在抽象类elevator中实现具体方法,是因为考虑到这些具体方法在不同子类电梯很固定,就直接在抽象类中实现,代码复用,简洁。如果子类实在需要修改,直接重写就好了。然后通过继承实现各类电梯。
通过接口实现以上的继承层次好像也行,毕竟接口里面也可以先实现具体方法。
ISP-Interface Segregation Principle
全局调度器有一个接口: 分配请求给局部队列
电梯可以分为两个接口:
- 产生指令的接口(其实是局部调度器)
- 执行指令的接口,包括如下方法: executeInstuciton, up, down, open, close, GetOnPerson, GetOffPerson
DIP-dependency Inversion Principle
我在这一点上做的也不好。
- 没有为scheduler内部的电梯引用设置接口,直接引用具体的电梯
- 在Input, Scheduler, Elevator内的电梯引用具体容器,没有为容器设置抽象接口List, Queue等
3. 基于度量分析程序结构
第一次作业
在第一次作业中,因为只有一个电梯,所以我的设计其中其实没有全局scheduler,图中的scheduler是采用look算法的局部scheduler。(~但我写第一次作业的时候我并没有意识到我写的scheduler只是一个局部的scheduler,我当时以为它在第二三次作业中可以是一个全局的~)
第一次作业中可以观察到局部调度器的look方法ev,iv,v三者都高,分别是因为在look方法是局部调度器的核心方法,结构复杂(非结构化程度高),调用其他方法较多(与其他模块耦合高), 分支多。
当使用一个复杂的策略算法时,如何能够保持代码结构化,低耦合,少分支,笔者目前还没有很好地想法。可能将look算法拆分为不同的几个阶段会稍微好一些。
第二次作业
在二次作业中,我才意识到我第一次中的scheduler是局部的,每个电梯都会有一个scheduler,负责根据电梯的状况和局部队列产生电梯的可执行指令。因此我把第一次作业的scheduler移动到电梯内部作为一个小模块(函数),然后重新写了一个全局的scheduler。
第二次作业中复杂度主要集中在COntroller.assign(), Elevator.look(), MainCLass.I2F(), 其实同第一次作业中的分析一样,这三者都是逻辑较为复杂,分支较多的模块,但是整个模块的各个部分之间联系紧密,难以拆分。笔者目前还没有太好的办法,若有读者知道,望能在评论区不吝赐教。
可能把Elevator里面的局部调度器封装成一个单独的类,可以降低ELevator.look的复杂度吧。
第三次作业
在第三次作业中,采用Worker模式,因此把电梯线程都交由COntroller来start。
第三次作业中,Elevator的构造函数复杂度较高。因为我的设计中Elevator是通过传入一个类型参数来决定ELevator的范围,速度,容量等,这导致构造方法分支过多。
其实现在想想,这种情况下,更好的选择应该是使用工厂方法模式。定义一个电梯抽象类或者接口。底下有几种不同类型的电梯,为每个类型的电梯定义一个工厂。
4. 分析自己程序的bug
三次作业中,只有在第一次作业的互测中发现bug。(~QAQ其实这个bug在我本地检查时就已经发现了,但是那一周玩的有点浪,发现的时候已经九点四十多,没有时间改了~)
其实就是check-then-act忘了加锁,也就是说只保护共享对象是线程安全类是不够的,还得保证线程执行方法中前后逻辑相关的代码是原子性的。
另外,在本地进行进行自动化测试时,发现了另一个bug,就是我在synchronized代码块里面sleep,导致其他某些线程长时间拿不到锁。只需设置标志变量,将sleep移出监控区即可。
5. 分析别人bug所采用的策略
本单元互测环节,我就基本采用的是
- 本地自动测评机随机生成数据,黑盒测试
- 本地自动测评机有针对性生成数据,黑盒测试
并没有发现roommate的任何bug,说明相较于第一单元,大家在如何检测自己的程序上有了很大的进步,roommate水平很高。
6. 心得体会
关于程序检测的心得
三次作业中,只有在第一次作业的互测中发现bug。(~QAQ其实这个bug在我本地检查时就已经发现了,但是那一周玩的有点浪,发现的时候已经九点四十多,没有时间改了~)
总结下来,我对自己的代码测试时有一套完整的流程,致使我最终提交的代码bug较少。
- 根据指导书手动构造有针对性的测试用例
- 本地自动测评机随机生成数据,黑盒测试
- 本地自动测评机有针对性生成数据,黑盒测试
- 通过JProfile观察线程的状态转变图(个人经验是如果状态转变图比较混乱,那么线程设计往往有bug)
- 小黄鸭检测法
- 网站测评
但说实话,我仍有如下做的不好的地方,在接下来两单元中需要改进:
- 缺乏白盒检测,并没有保证每一个分支都运行到。笔者将要学习Junit进行单元测试
- 第三次作业由于懒惰,没有构建关于乘客等待时间的计时器,这也是导致我第三次作业性能不理想的间接原因
- 没有仔细观察网站测评的反馈结果,网站测评的反馈结果有时候可以看出线程设计和性能的不足
线程安全
个人认为第二单元多线程电梯调度比第一单元表达式求导难很多,难就难在多线程不经意间就会在安全性上给你挖一个大坑。特别是前两次作业,我本地发现的所有bug无一例外全是和线程安全相关的。
但实际上,到了第三次作业,我掌握了多线程设计的一些套路,设计就容易了很多,也没有出现安全性问题:
- 将所有共享对象设计成线程安全类
- 将相关联的一组操作设计为原子操作
- 将锁设计为与监控区内计算相关的对象
- 尽量缩小监控区范围,将sleep或其他耗时的计算操作通过标志变量移出监控区。
- 观察JProfile的线程转化图和其他图表
我在三次作业中,所有的共享对象(请求队列,电梯的状态)都没有设计成线程安全类, 而是采用对关联代码块手动上锁的模式,这样做牺牲了代码的可维护性和可扩展性,不可取。
设计原则
笔者在SRP做的比较好。而在OCP以及具体的实现LSP,ISP,DIP做的都不够好。具体的分析和改进可以见2.3节。一句话总结下来,就是不要面向具体实现类编程,要面向接口和抽象类编程,这样的程序才能具有好的扩展性
接下来要学习的
- Junit单元测试
- lock
- JML