- 前言
- HW5
- 度量分析
- UML类图与协作图
- bug分析
- HW6
- 度量分析
- UML类图与协作图
- bug分析
- HW7
- 度量分析
- UML类图与协作图
- bug分析
- SOLID原则
- 感想
前言
紧张刺激的第二单元结束了,本单元体验极佳,
进行作业前有注重架构的稳定,在良好的架构上考虑优化,如此也便利了迭代。
另外,没遇上线程安全bug的电梯,不是完整的电梯[doge]
HW5
本次作业要求实现单部可捎带电梯,典型的生产者消费者模式,
输入线程Request
是生产者,
电梯线程Elevator
是消费者,
“托盘”RequestQueue
负责协调加锁,
电梯运行模式采用的是look
,
电梯在可达楼层上下扫描,
当前方向无请求且电梯内无人则掉头,
两边都无请求则等待,
捎带规则是请求与电梯方向(上行或下行)一致且处于同一楼层。
度量分析
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.run() | 5 | 6 | 8 |
RequestQueue.ifChangedir(int,boolean) | 6 | 4 | 6 |
Total | 43 | 65 | 75 |
Average | 1.54 | 2.32 | 2.68 |
Class | OCavg | WMC |
---|---|---|
Elevator | 3 | 27 |
Floor | 1.14 | 8 |
MainClass | 1 | 1 |
Request | 1.67 | 5 |
RequestQueue | 3.4 | 17 |
Stop | 1 | 3 |
本次作业功能较少,复杂度整体不高,控制了每个方法的规模都不大于30行,
标红的只有Elevator.run()和RequestQueue.ifChangedir(int,boolean),
run方法其实已经是只管调用方法的方法了,主要是涉及结束判断以及本身电梯运行动作就很多,
而转向方法主要是为了实现look
算法,通过电梯运行方向以及遍历楼层寻找请求返回是否转向标志。
RequestQueue的OCavg也标红,循环复杂度比较高,楼层判空以及电梯转向都是循环+判断,
或许可以单开一个电梯控制类负责电梯行为,分担复杂度。
UML类图与协作图
可以看到,主类只负责创建对象,
Request
和Elevator
实现了Runnable
接口,二者通过RequestQueue
进行交互,
RequestQueue
持有楼层对象Floor
,每一层楼用一个Floor
模拟,拥有上行和下行两条线程安全队列。
bug分析
-
自己的bug:本次作业中测阶段发现了一个粗心大意的bug,楼层初始化应该放在电梯启动前,不然会出现大面积NullpointerException,
强测以及互测未发现bug -
屋内其他人的bug:发现了一个人捎带时总会漏一两个人,导致时间比其他人慢一大截,勾起了兴趣,
仔细研究后,发现在同一时间输入同一楼层请求还可能会少完成请求,具体原因没有找到,
hack成功一次,屋内一共三刀,以为都是同一个人,结果发现是三个人的bug,
多线程真是什么奇奇怪怪的bug都有。
HW6
第二次作业加入电梯的数量可以是1-5台,
单部电梯采取的策略还是选择了沿用第一次作业的look,多部电梯每一部一个线程,
除了电梯线程和输入线程外还增加了一个主调度线程MasterScheduler
,负责任务的分派。
考虑到如果有多部电梯且电梯还有容量限制,
请求集中在少部分电梯上会造成忙闲不均浪费资源,因此负载均衡会取得很好的收益,
所以电梯与主调度器之间还应共享一个电梯状态对象ElevatorStatus
,实例化在子调度器中,调度函数依据此来分派请求,以期取得较优效率。
具体来讲,就是根据实际在电梯里的人数,对应楼层已经分派的人数以及电梯获得请求后需要移动的距离作为参考条件计算惩罚值,
惩罚值最小的电梯将获得此请求,
这几个条件还需要加权,经历了调参之后发现电梯人数所占的比例应该稍大一些,最后取得的性能分还不错。
本次作业原本想使用可重入锁ReentrantLock
来灵活的进行await和signal,
但是可能是理解得不够透彻,没能顶住例行大数据大范围轰炸,
出现了不可复现的线程安全问题,主要是只出现了一次,没能记录下来,之后干脆synchronized
简单明了。
度量分析
Method | ev(G) | iv(G) | LOC | v(G) |
---|---|---|---|---|
MainClass.main(String[]) | 1 | 1 | 35 | 1 |
MasterScheduler.dispatcher(PersonRequest) | 1 | 27 | 45 | 27 |
Total | 76 | 128 | 481 | 141 |
Average | 1.43 | 2.42 | 9.08 | 2.66 |
Class | OCavg | WMC |
---|---|---|
Elevator | 2.89 | 26 |
ElevatorStatus | 1 | 9 |
Floor | 1.12 | 9 |
MainClass | 1 | 1 |
MasterScheduler | 4.6 | 23 |
Request | 1.5 | 6 |
Scheduler | 3.12 | 25 |
Stop | 1 | 3 |
Tray | 1.5 | 9 |
复杂度相较于第一次不升反到总体上有些下降,良好的设计果然会给编程带来帮助。
这次标红的主要是分派策略上dispatcher
方法,也是唯一一个突破40行的方法,
主要因为是计算电梯运送请求距离的时候情况太多,
本次作业也是有意限制方法功能,可以看到方法行数超过30的只有两个方法,
main不算,初始化构造函数太长了没办法
UML类图与协作图
依然采取生产者消费者模式,不过这一次是两级的,
输入与主调度之间有一个简单的共享对象Tray
负责协调、
主调度和每部电梯之间都有一个共享对象Scheduler
负责协调,这个共享对象实现了子调度器接口ChildScheduler
。
由于主调度器负责任务分派,因此调度函数实现在主调度器中,电梯对于请求分派不可见,只管跑就完事了。
Stop
信号也是两级的,电梯只有在输入线程结束,以及主调度器中请求分派结束、子调度器无请求而且电梯内无人的情况下才能结束。
bug分析
-
自己的bug:本次在公测阶段没有遇到bug,互测阶段被hack了一个线程安全问题,
实际上现在我也很疑惑,为什么会在pcj跑出电梯突然中途等了很久又开始运行的结果???
本地pcj跑了一晚上也没有复现,但是由于这个罕见的bug,我决定肉眼debug,确实发现了两个小概率发生的问题,
其中一个比较好描述,就是电梯更新状态的时候连续两次调用改变同一状态变量的synchronized方法,
两次调用之间可能会发生线程切换,将会使用旧的值。 -
发现别人的bug,这次盲Caster,具体原因读了代码以及本地尝试复现均没有成功发现
HW7
第三次作业最大的不同在于动态加入电梯并且有ABC三种可达楼层不相同电梯,有些请求需要电梯之间的协作才能完成。
这次基调还是生产者消费者思想,受教学视频点拨,又采用了workerThreads
模式,
在上一次的基础上将电梯线程创建和启动放置在主调度器MasterScheduler
中,
同时为每部电梯实例化一个子调度器Scheduler
作为“托盘”。
这次的调度还是秉承着负载均衡的原则,
事先决定A类电梯作为负数楼层和15-20层之间的高速列车,
B类主要负责出发地是偶数楼层的请求,
C类主要负责出发地是奇数楼层的请求,
当然,笔者首先在主调度器中对请求进行一定粒度的分割Dispatch
,让目的地在相应电梯的可达楼层范围之内,
每个子调度器在上一次的基础上增加一个nextStage
线程安全队列用于储存被分配到的具有依赖关系的请求,
如此一来,换乘问题只需要分割请求、
完成先决请求后回调另外两类子调度器(例如B类子调度器通知AC类)、
进行下一阶段请求这样的系列操作就得以解决。
同类电梯之间也涉及到调度,笔者直接在子调度器复用了第二次作业的调度函数,最终达到的效果也差强人意。
度量分析
Method | ev(G) | iv(G) | LOC | v(G) |
---|---|---|---|---|
Scheduler.dispatcher(PersonRequest) | 1 | 28 | 48 | 28 |
Total | 111 | 190 | 777 | 250 |
Average | 1.56 | 2.68 | 10.94 | 3.52 |
Class | OCavg | WMC |
---|---|---|
Dispatch | 6 | 18 |
Elevator | 3.79 | 53 |
ElevatorStatus | 1.22 | 11 |
Floor | 1.12 | 9 |
MainClass | 1 | 1 |
MasterScheduler | 2.8 | 28 |
Output | 1 | 1 |
RequestIn | 1.67 | 5 |
Scheduler | 3.53 | 53 |
Stop | 1 | 3 |
Tray | 1.75 | 7 |
复杂度有小幅度上升,主要是分派策略是静态的,而且用的都是大面积ifelse,
受其他dalao的启发,其实是可以换成静态数组或者其他容器,直接查询即可。
本次方法规模仍然有意在控制,事实证明控制方法规模有利于定位bug。
UML类图与协作图
从类图中可以看到其实架构相较于第二次并没有太多的改变,
就是多了输出接口的封装以及将分派方法的抽取单独的类,
让主调度器拥有一个单例,也是想到了SOLID原则。
协作图大体不变,只是多了在主调度器以及子调度器两个层次进行的分派
bug分析
-
自己的bug:本次作业公测、互测阶段均没有发现bug
-
别人的bug:此次A屋质量可能堪忧???
屋内b叔的判断超载的条件形同虚设还爆出神奇RE,
Lancer的电梯会因为线程安全莫名鬼畜导致rtle。
只有超载是看懂了的,其他的bug恕我能力不足,真的不知道怎么发生的。。。
SOLID原则
-
单一责任原则
- 输入线程负责读取输入并输送至主调度器以及向主调度器发送结束信号;
- 主调度器负责将指令按调度策略分配给电梯、添加电梯并传递结束信号;
- 电梯从子调度器取指令、向子调度器返回电梯状态以及主动运行。
- 子调度器充当电梯与主调度器之间的通讯媒介,储存指令以及电梯运行状态。
-
开放封闭原则
- 本单元迭代体验极佳,
除了第二次在第一次的基础上单独开辟一条主调度器线程配备“托盘”通讯外几乎没有改动
- 本单元迭代体验极佳,
- 电梯类以及子调度器都是从本单元第一次作业复用下来的,通过恰当控制函数就可以使用。
-
输入线程无过多改动,根据接口对应增加需求即可。
-
楼层类从第一次作业一直用到了最后一次作业,几乎没有改动。
-
第三次作业的电梯状态类原封不动地源自第二次作业
-
里氏替换原则:本单元没有用到继承。
-
接口分离原则:线程实现Runnable接口,
Scheduler
实现ChildScheduler
接口,没有臃肿的接口。 -
依赖倒置原则:
Elevator
和MasterScheduler
依赖于负责通讯的实现ChildScheduler
接口的Scheduler
,
如果遇到新需求需要新种类子调度器的话可以减少改动。
感想
本单元感想就是一个字儿,爽!!
课程视频助我探索多线程的设计模式!!!(迭代过程思路清晰,基本周二晚上可以出第一版
考虑优化助我走出舒适区!!!(虽然做的也不是很好了
莫名死锁助我加强测试、push我深入代码的字里行间进行探索!!!
评测组强大的pcj助我push别人修神必的锅!!!
OO与知乎上所说的“远古”OO已经大不一样了,希望OO越走越好!!!