第二单元总结博客
多线程协同和同步控制分析
三次作业我主要实现了生产者-消费者模式,维护了三种线程:主线程、输入线程(生产者)、电梯线程(消费者),调度器与请求队列相结合,输入线程拿到输入后加入请求队列,电梯运行到某一层后,从请求队列中取走请求,而主要的同步控制发生在调度器中,需要把对请求队列的操作进行同步。本次作业我没有出现死锁的情况,一方面确实有运气的成分,另一方面我的共享资源也并不多,对共享资源的操作也比较简单,不易产生死锁条件。
这次作业多线程方面存在的问题有:
-
synchronized关键字全部作用在了方法上,粒度太大。第一次作业为了求稳,也是对“锁”的概念理解不深,所有的synchronized关键字全部作用在了方法上,后两次为了保证稳定也没有细化同步粒度或是自己实现lock,不得不说是一个遗憾。
-
调度器直接调用了Elevator类。尽管一直试图避免线程类直接与调度器交互,但还是没能成功,调度器也会直接操作电梯的destination,这一点也是设计上的一大问题。
第三次作业的功能设计与性能设计的平衡分析
实话说,第三次作业仅仅是实现功能就已经让我感到头秃了。思考了一下作为一个人,我选择换乘时的心路历程,我采用的策略是,电梯到达某一层后,请求先进入电梯,然后把自己的目标楼层或者下一换成楼层作为在这一电梯上的终点。考虑到本次作业中所有的情况都不会出现A-B-A类的换乘情况,我在每个请求中加入了一个队列,来记录自己上过哪个电梯,如果该电梯之前上过,那么就不再乘坐,一定程度上也避免了一个人在楼层和电梯间反复横跳的诡异情景。有些同学采用了静态调度的策略,但是我认为这样的效果未必好,无论是用图解出来了最短路还是打表,最后的结果都是可能要死等某一类电梯来。至少现实世界中,如果不知道下一辆直达的电梯何时才能到达,我们大多数人可能宁可乘上一辆电梯先去换乘站也不愿干等着。或许可能有更好的算法,但本人确实水平有限,实在想不到了。最终我的这一算法得到的性能分是98+,虽不能和大佬们99+相比,但也比较令我自己满意了。
这样性能上简单的设计,功能上也没有让我大改架构,只是在MyRequest类中加入了一个寻找在这一电梯中要到达的楼层的方法罢了,其他的地方与前两次作业倒是大同小异。
用SOLID原则分析自己的作业
- 单一职责原则:基本做到了。但调度器类还会负责当电梯停止时确定电梯的下一目的地,这一功能理应由电梯类实现。
- 开闭原则:基本做到了,三次作业中我对上一次代码的改变都比较小,算是对扩展开放了。
- 里氏替换原则:没有使用继承。
- 依赖倒置原则:本次作业中,我在设计的时候没太感知到这一原则,用的几个类也是并行关系,不能说是高低层次的关系,对于这一原则还需要进一步理解。
- 接口分离原则:未使用接口,因为本次作业的功能往往是某一个类所特有的。
三次作业的度量分析
总的来说,由于第一次作业前仔细思考了架构,三次作业都是迭代完成,从类图中也可以看出来基本没有架构上的改进,每次迭代开发时也只需要更改有限的代码,总体功能也都是大同小异的。除了第三次作业因为要实现不同的电梯所以引入了一个电梯工厂类,其余的架构几乎一样。
第一、第二次作业时序图:
第一次作业
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM | FANIN | FANOUT |
---|---|---|---|---|---|---|---|---|---|---|---|
Elevator | 8 | 0 | 16 | 16 | 135 | 32 | 0 | 0 | 0 | 0 | 0 |
Input | 3 | 0 | 2 | 2 | 37 | 5 | 0 | 0 | 0 | 0 | 0 |
MainClass | 0 | 0 | 1 | 1 | 12 | 1 | 0 | 0 | -1 | 0 | 0 |
Request | 3 | 0 | 3 | 3 | 16 | 3 | 0 | 0 | 0 | 0 | 0 |
Scheduler | 4 | 0 | 6 | 6 | 128 | 28 | 0 | 0 | 0 | 0 | 0 |
Type Name | Method Name | LOC | CC | PC |
---|---|---|---|---|
Elevator | Elevator | 3 | 1 | 1 |
Elevator | setDestination | 4 | 1 | 1 |
Elevator | setDestination | 12 | 3 | 0 |
Elevator | setState | 12 | 3 | 0 |
Elevator | arrive | 3 | 1 | 0 |
Elevator | open | 3 | 1 | 0 |
Elevator | close | 3 | 1 | 0 |
Elevator | out | 9 | 3 | 0 |
Elevator | in | 15 | 3 | 0 |
Elevator | move | 9 | 3 | 0 |
Elevator | judgeOpen | 3 | 1 | 0 |
Elevator | setHasIn | 3 | 1 | 1 |
Elevator | run | 36 | 7 | 0 |
Elevator | getState | 3 | 1 | 0 |
Elevator | getPresentFloor | 3 | 1 | 0 |
Elevator | getDestination | 3 | 1 | 0 |
Input | Input | 5 | 1 | 2 |
Input | run | 27 | 4 | 0 |
MainClass | main | 10 | 1 | 1 |
Request | Request | 5 | 1 | 3 |
Request | getDirection | 3 | 1 | 0 |
Request | getRequest | 3 | 1 | 0 |
Scheduler | putRequest | 31 | 6 | 1 |
Scheduler | getRequest | 43 | 10 | 1 |
Scheduler | setHasNextInput | 4 | 1 | 1 |
Scheduler | getHasNextInput | 5 | 1 | 0 |
Scheduler | getNearestFloor | 13 | 3 | 1 |
Scheduler | getNearestDirFloor | 26 | 7 | 2 |
本次作业的getRequest的圈复杂度略高,原因在于这一方法不仅实现了取楼层,当电梯为空时还要判断下一目的地,造成功能太多,这也违反了单一职责原则。
第二次作业
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM | FANIN | FANOUT |
---|---|---|---|---|---|---|---|---|---|---|---|
Elevator | 10 | 0 | 19 | 19 | 158 | 37 | 0 | 0 | 0 | 0 | 0 |
Input | 3 | 0 | 2 | 2 | 37 | 5 | 0 | 0 | 0 | 0 | 0 |
MainClass | 0 | 0 | 1 | 1 | 16 | 2 | 0 | 0 | -1 | 0 | 0 |
Request | 3 | 0 | 3 | 3 | 16 | 3 | 0 | 0 | 0 | 0 | 0 |
Scheduler | 6 | 0 | 6 | 6 | 84 | 16 | 0 | 0 | 0 | 0 | 0 |
Type Name | Method Name | LOC | CC | PC |
---|---|---|---|---|
Elevator | Elevator | 3 | 1 | 1 |
Elevator | Elevator | 4 | 1 | 2 |
Elevator | setDestination | 4 | 1 | 1 |
Elevator | setDestination | 12 | 3 | 0 |
Elevator | setState | 12 | 3 | 0 |
Elevator | arrive | 3 | 1 | 0 |
Elevator | open | 3 | 1 | 0 |
Elevator | close | 3 | 1 | 0 |
Elevator | out | 10 | 3 | 0 |
Elevator | in | 16 | 3 | 0 |
Elevator | move | 19 | 5 | 0 |
Elevator | judgeOpen | 3 | 1 | 0 |
Elevator | setHasIn | 3 | 1 | 1 |
Elevator | run | 35 | 7 | 0 |
Elevator | getState | 3 | 1 | 0 |
Elevator | getPresentFloor | 3 | 1 | 0 |
Elevator | getDestination | 3 | 1 | 0 |
Elevator | getPassengerCnt | 3 | 1 | 0 |
Elevator | getName | 3 | 1 | 0 |
Input | Input | 5 | 1 | 2 |
Input | run | 27 | 4 | 0 |
MainClass | main | 14 | 2 | 1 |
Request | Request | 5 | 1 | 3 |
Request | getDirection | 3 | 1 | 0 |
Request | getRequest | 3 | 1 | 0 |
Scheduler | Scheduler | 8 | 2 | 1 |
Scheduler | putRequest | 14 | 2 | 1 |
Scheduler | getRequest | 32 | 7 | 1 |
Scheduler | setHasNextInput | 4 | 1 | 1 |
Scheduler | getHasNextInput | 5 | 1 | 1 |
Scheduler | getNearestFloor | 13 | 3 | 2 |
本次作业各方面控制的都比较好,除了出现了“幻数”的问题。主要是由于到达的楼层和sleep时间是直接用数表示的。
第三次作业
Type Name | NOF | NOPF | NOM | NOPM | LOC | WMC | NC | DIT | LCOM |
---|---|---|---|---|---|---|---|---|---|
Elevator | 15 | 0 | 20 | 20 | 193 | 43 | 0 | 0 | 0 |
ElevatorFactory | 1 | 0 | 2 | 2 | 30 | 7 | 0 | 0 | 0 |
Input | 3 | 0 | 2 | 2 | 47 | 7 | 0 | 0 | 0 |
MainClass | 0 | 0 | 1 | 1 | 20 | 1 | 0 | 0 | -1 |
MyRequest | 11 | 0 | 11 | 11 | 151 | 41 | 0 | 0 | 0 |
Scheduler | 6 | 0 | 9 | 9 | 128 | 30 | 0 | 0 | 0.222222 |
Method Name | LOC | CC | PC |
---|---|---|---|
Elevator | 10 | 1 | 8 |
setDestination | 4 | 1 | 1 |
setDestination | 12 | 3 | 0 |
setState | 12 | 3 | 0 |
arrive | 3 | 1 | 0 |
open | 3 | 1 | 0 |
close | 3 | 1 | 0 |
out | 17 | 4 | 0 |
in | 17 | 3 | 0 |
move | 18 | 5 | 0 |
judgeOpen | 3 | 1 | 0 |
setHasIn | 3 | 1 | 1 |
run | 49 | 11 | 0 |
getState | 3 | 1 | 0 |
getPresentFloor | 3 | 1 | 0 |
getDestination | 3 | 1 | 0 |
getPassengerCnt | 3 | 1 | 0 |
getName | 3 | 1 | 0 |
getType | 3 | 1 | 0 |
getMaxPassenger | 3 | 1 | 0 |
ElevatorFactory | 3 | 1 | 1 |
makeElevator | 24 | 6 | 2 |
Input | 5 | 1 | 2 |
run | 37 | 6 | 0 |
main | 18 | 1 | 1 |
MyRequest | 19 | 4 | 2 |
getIn | 6 | 1 | 2 |
getOff | 11 | 3 | 1 |
setTmpDest | 36 | 10 | 0 |
getAnotherExchangeFloor | 23 | 8 | 1 |
getTmpDest | 3 | 1 | 0 |
getAlreadyGet | 3 | 1 | 0 |
eleTypeCanGetOff | 11 | 4 | 1 |
getRequest | 3 | 1 | 0 |
getTmpStartFloor | 3 | 1 | 0 |
canBeNextDes | 20 | 7 | 2 |
Scheduler | 14 | 4 | 0 |
putAllRequests | 4 | 1 | 1 |
removeAllRequests | 4 | 1 | 1 |
putRequest | 11 | 2 | 1 |
getRequest | 35 | 8 | 1 |
setHasNextInput | 4 | 1 | 1 |
getHasNextInput | 7 | 2 | 0 |
getNearestFloor | 33 | 9 | 2 |
getFloorNumber | 8 | 2 | 2 |
第三次作业个别方法的圈复杂度过大。
第三次作业时序图:
自己的bug
三次作业强测正确性均为100%,互测也只在第一次出现了bug。这一bug的形成原因是:在电梯移动时,我先输出了ARRIVE,再sleep(400),这样就会出现ARRIVE的时间戳在0.4s以内的情况,而这一情况显然是不符合电梯的移动规律的。交换二者顺序后顺利修复了这一bug。之所以会产生这一bug,一方面是设计时忽略了电梯应当先移动再到达,这样也符合现实世界的规律。另一方面也是自己第一次作业搭建的评测机不支持对时间戳的正确性进行判断,所以也未能检查出问题。
其他在课下发现的bug,主要集中在线程的终止条件。例如第一次第二次作业,需要考虑到电梯线程的终止条件是没有输入and请求队列为空and电梯内没人。而第三次作业要考虑到换乘,延续前两次作业的终止条件,则会导致有的电梯线程提前终止,需要换乘的请求下了电梯无路可去。此时应当单独维护一个ArrayList,用于记录未完成的请求。
这一单元下来,我对自己搭建的评测机比较满意。搭建评测机时也同样用到了面向对象的思想,让我感到对象的抽象确实能够大大降低开发难度。此外我的评测机也实现了对不同问题的报错,并且可以定位到时间戳、哪一部电梯在哪一层出了问题,尽管这些比较棒的设计并没有帮我发现太多别人的bug,但还是帮自己找到了TLE的问题。
别人的bug
三次互测都没有发现其他同学的bug。在互测中,我主要依靠自动测试程序找bug,但是效果并不理想,因为随机数据往往难以覆盖到边界数据,而观察同组中其他同学hack的样例,发现问题也都是出现在比较极端的情况下。未来自己的评测机搭建的时候,可以有意识地构建多组极端情况的数据进行测试,或许可以hack到别人。此外,多线程的不确定性也增大了hack的难度,例如第三次作业中,我确定某位同学的程序会出现RTLE的情况,但是同样的数据,即使在我自己的电脑上也很难复现RTLE的结果,反复提交几次也未能hack成功。
但是读其他同学的代码还是让我有所收获。第二次作业中,一位同学尝试了“虚拟电梯”结合贪心算法,调度性能大大提升,让我深受启发,虽然在第三次作业中,我并没有能够成功运用这一算法 : (
心得体会
线程安全
实现线程安全,需要考虑到共享资源有哪些,有哪些线程会使用这些共享资源,对这些共享资源是读还是写。这些考虑清楚后,在使用锁的过程中需要正确地用锁,防止出现死锁。此外,线程终止的判断条件也至关重要。
设计原则
这一单元确实感觉到了好的设计原则对于迭代开发的重要性。在首次开发前就参照这些原则进行设计,也大大降低了后面的设计难度。遵守设计原则时,迭代的难度曲线比较平缓;而本次作业中违反了设计原则的方面,也能让我感觉到它们是迭代开发的硬骨头。迭代开发中,“增”要比“改”简单得多,这大概是开闭原则的最好体现。
这一单元感觉初入了Java多线程开发的大门,感谢老师助教的辛勤付出!