OO第二单元总结
17231041 金陆洋
这一单元开启了多线程的旅途,从最开始的FAFS,到ASL,再到最后的SS,每一次的难度都有一个很大的提升,尤其是最后一次作业的时候,经常会让我领略到凌晨四点的北航(当然,也有OS的功劳),当然,在这个痛苦的过程中,我的能力也得到了一次又一次的提升(应该吧~~~),当然和大佬相比还是有很大的差距。
接下来进入正题,先来讲一讲这三次作业中对多线程的一些个人拙见,可能有些片面,各位看官笑笑即可,如若能在评论区指点几句就更加感激不尽了。
与暴力轮询say goodbye——从让它睡觉到让它等着
面对共享对象(在这次作业中,即RequestList)的访问,一个永远逃不开的问题就是如何避免暴力轮询,一种方式是sleep(time)让线程休眠一段时间,另一种方法是wait/notify使线程暂时阻塞直到被唤醒。
由于第一次作业中我不太敢用wait/notify,对这两个方法的功能也不甚了解,因此采用了简单粗暴的sleep()方法,让电梯分配器为空的时候每访问一次RequestList便休眠一段时间才能进行下一次的访问,其实这样做似乎没什么问题,更准确地说,正确性并没有受到影响,如此简易高效的做法我本可能沿用到之后的两次作业中的,直到某位大佬对我说了这样一句话:
感觉你这样做不够美观。
使用sleep固然不会错,可这样做好吗,答案显然应该是否定的。
或许一个电梯的时候sleep还可以,但多电梯呢?多调度策略呢?很显然,sleep不能很好地支持代码的健壮性和可复用性,为了面对将来纷繁复杂的情况,我们显然应该使用wait/notify来解决轮询。
这就好比人家给了我点钞机,我却要一张一张自己点,一张两张效率可能更高,但一万张呢?
因此,从第二次作业开始,我换用了wait/notify,其实真的蛮好用的,尤其是第三次作业的时候,而且似乎wait/notify的性能比单纯sleep还要好一点?
其实sleep有个值得注意的问题,如果我哪天脑抽把sleep写在了synchronized方法中,那可能真的gg了。
在使用wait/notify的时候,我也越加深刻地体会到了代码构架的重要性,只有将构架完整地搭建好了,使用wait/notify才会真正万无一失。
因此,真正好的构架应该是一以贯之行云流水的的,而绝不是缝缝补补拆东墙补西墙的,这在我的第二次电梯作业中体现得最为明显,在这次作业中,我第一次使用了wait/notify,自然少不了四处碰壁,也发现了构架的不少小问题,我选择了直接重构,其实也并未花费多长时间,反而可能比一遍又一遍地找bug更快,而且从根本上解决了众多bug(虽然性能分不高)。但其实如果我能在写代码之前再认真思考一下架构问题,而不那么急于动手,应该也就不需要这一次的重构了,也算是一个教训了。
于是,第三次电梯作业我用了一个晚上从头到尾地做好了构架,真正写代码的时候反而很轻松。
可以说,时至今日,我才真正体会到了一个好的构架的重要性。
不要重复造轮子,指导书中如是说,我原来不明白这句话的真正含义,现在才发觉,原来我之前都在重复造轮子。
多线程冲突——synchronized大法真的好吗
作为Java尤其是多线程的初学者,只要涉及到共享对象的访问就加一个synchronized,这样似乎万事大吉了?
如果没有一个良好的构架作为基础,那么势必会导致synchronized关键字出现频率异常多,而互斥锁用得越多,也就更有可能出现难以预料的线程问题,这是在所难免的。
因此,synchronized应该是用得越少越好(当然,在保证线程安全的前提下),这也就需要一个好的架构来实现解耦,这样,似乎又回到了上一个问题。
解决线程冲突问题,也可以使用不可变对象,这也是行之有效的方法,但由于电梯作业的增删改查过于频繁,使用不可变对象的代价太大,因此最终放弃了这个想法(应该是我太菜了没有想到使用的方法)。
线程的结束——一起停还是分别停
这个问题,其实就不是那么重要了,两种策略在我看来无孰优孰劣,根据自己的构架选择更加适合的即可。
我使用的是分别停止的方法,在每个电梯完成了所有的工作后(并且不会再有新的请求)即可使线程结束,所有线程结束,程序正常退出。
FAFS电梯总结
这一次的作业较为简单,Schedule作为共享对象,InputHandler进行存储,Elevator进行拿取(由于比较简单,我没有使用Dispatcher,可拓展性并不够好)。
method | ev(G) | iv(G) | v(G) |
oo.Elevator.changeClose() | 1.0 | 1.0 | 1.0 |
oo.Elevator.pickUp() | 1.0 | 6.0 | 6.0 |
oo.Elevator.run() | 5.0 | 7.0 | 8.0 |
oo.Elevator.send() | 1.0 | 6.0 | 6.0 |
oo.InputHandle.run() | 3.0 | 4.0 | 4.0 |
oo.Main.main(String[]) | 1.0 | 1.0 | 1.0 |
oo.Schedule.getOut() | 1.0 | 1.0 | 1.0 |
oo.Schedule.getState() | 1.0 | 1.0 | 1.0 |
oo.Schedule.goInto(int,int,int) | 1.0 | 1.0 | 1.0 |
Total | 15.0 | 28.0 | 29.0 |
Average | 1.67 | 3.11 | 3.22 |
class | OCavg | WMC |
oo.Elevator | 3.5 | 14.0 |
oo.InputHandle | 3.0 | 3.0 |
oo.Main | 1.0 | 1.0 |
oo.Schedule | 1.0 | 3.0 |
Total | 21.0 | |
Average | 2.33 | 5.25 |
只有Elevator的run方法复杂度较高,这是因为我将分配器(Dispatcher)以及电梯(Elevator)合二为一,没有实现真正的解耦。
bug:
本次作业较为简单,并未发现自己和他人的bug。
ASL电梯总结
由于水平较为有限,也比较怕互测被极端数据卡时间,因此采用了官方推荐ASL算法。
这次电梯作业,我采取了Dispatcher和Elevator分离的构架。
RequestList作为共享对象(即容器),InpurHandler发送新的请求信息,Dispatcher负责获取所需信息并调度电梯(Elevator)。
注:分配器是线程而电梯不是线程,避免两个线程间的安全问题。
method | ev(G) | iv(G) | v(G) |
oo.Dispatcher.carryGo() | 4.0 | 7.0 | 9.0 |
oo.Dispatcher.Dispatcher(RequestList) | 1.0 | 1.0 | 1.0 |
oo.Dispatcher.emptyGo() | 1.0 | 5.0 | 7.0 |
oo.Dispatcher.goOut(int) | 1.0 | 3.0 | 3.0 |
oo.Dispatcher.goOutEm(int) | 1.0 | 4.0 | 4.0 |
oo.Dispatcher.into(ArrayList<arraylist>) | 1.0 | 2.0 | 2.0 |
oo.Dispatcher.inWait() | 1.0 | 2.0 | 2.0 |
oo.Dispatcher.run() | 4.0 | 5.0 | 5.0 |
oo.Elevator.close(int) | 1.0 | 2.0 | 2.0 |
oo.Elevator.getFlag() | 1.0 | 1.0 | 1.0 |
oo.Elevator.move(int) | 1.0 | 2.0 | 2.0 |
oo.Elevator.open(int) | 1.0 | 3.0 | 3.0 |
oo.InputHandler.InputHandler(RequestList) | 1.0 | 1.0 | 1.0 |
oo.InputHandler.run() | 3.0 | 4.0 | 4.0 |
oo.Main.main(String[]) | 1.0 | 1.0 | 1.0 |
oo.RequestList.getNum() | 1.0 | 1.0 | 1.0 |
oo.RequestList.getOut() | 1.0 | 3.0 | 3.0 |
oo.RequestList.goInto(int,int,int) | 1.0 | 1.0 | 1.0 |
oo.RequestList.lookUp(int,int,int) | 1.0 | 13.0 | 13.0 |
Total | 27.0 | 61.0 | 65.0 |
Average | 1.42 | 3.21 | 3.42 |
class | OCavg | WMC |
oo.Dispatcher | 4.0 | 32.0 |
oo.Elevator | 1.5 | 6.0 |
oo.InputHandler | 2.0 | 4.0 |
oo.Main | 1.0 | 1.0 |
oo.RequestList | 2.75 | 11.0 |
Total | 54.0 | |
Average | 2.84 | 10.8 |
可以看出,对于共享对象的遍历查询(即捎带过程)的复杂度较高(表格中的lookUp方法),其实这在我的意料之中,因为判断每一请求能否捎带的确需要多重判断条件,现在想一想,把这些判断条件单独封装起来应该会更好,这是一个不足的地方。
另附上代码:
public synchronized ArrayList<ArrayList<Integer>> lookUp( int now, int waiting, int to) { ArrayList<ArrayList<Integer>> result = new ArrayList<>(); for (int i = 0; i < requestList.size(); i++) { ArrayList<Integer> tem = requestList.get(i); if (tem.get(1) == now) { if (now < to && now < tem.get(2)) { result.add(requestList.remove(i)); i--; } else if (now > to && now > tem.get(2)) { result.add(requestList.remove(i)); i--; } else if (waiting != 0 && now > tem.get(2) && tem.get(2) > waiting) { result.add(requestList.remove(i)); i--; } else if (waiting != 0 && now < tem.get(2) && tem.get(2) < waiting) { result.add(requestList.remove(i)); i--; } } } return result; }
bug:
由于我并未做实质上的优化,故强测成绩并不高,大家可能都没有测评机,因此并未发现bug。
SS电梯总结
这次作业我复用了上次作业的ASL调度算法作为每一个电梯的调度策略并扩展成了三部电梯(使用了三个分配器)。
看起来有点复杂,其实与上次作业的区别仅在于使用了三个分配器,另外我新建了SyOutput类为输出方法加上了锁以避免不安全的问题(但似乎没必要?)。
method | ev(G) | iv(G) | v(G) |
oo.DispatcherFirst.carryGo() | 7.0 | 10.0 | 12.0 |
oo.DispatcherFirst.DispatcherFirst(RequestList) | 1.0 | 1.0 | 1.0 |
oo.DispatcherFirst.emptyGo() | 1.0 | 5.0 | 7.0 |
oo.DispatcherFirst.goOut(int) | 1.0 | 4.0 | 4.0 |
oo.DispatcherFirst.goOutEm(int) | 1.0 | 5.0 | 5.0 |
oo.DispatcherFirst.into(ArrayList<arraylist>) | 1.0 | 2.0 | 2.0 |
oo.DispatcherFirst.inWait() | 1.0 | 2.0 | 2.0 |
oo.DispatcherFirst.run() | 4.0 | 5.0 | 5.0 |
oo.DispatcherSecond.carryGo() | 7.0 | 10.0 | 12.0 |
oo.DispatcherSecond.DispatcherSecond(RequestList) | 1.0 | 1.0 | 1.0 |
oo.DispatcherSecond.emptyGo() | 1.0 | 5.0 | 7.0 |
oo.DispatcherSecond.goOut(int) | 1.0 | 4.0 | 4.0 |
oo.DispatcherSecond.goOutEm(int) | 1.0 | 5.0 | 5.0 |
oo.DispatcherSecond.into(ArrayList<arraylist>) | 1.0 | 2.0 | 2.0 |
oo.DispatcherSecond.inWait() | 1.0 | 2.0 | 2.0 |
oo.DispatcherSecond.run() | 4.0 | 5.0 | 5.0 |
oo.DispatcherThird.carryGo() | 7.0 | 10.0 | 12.0 |
oo.DispatcherThird.DispatcherThird(RequestList) | 1.0 | 1.0 | 1.0 |
oo.DispatcherThird.emptyGo() | 1.0 | 5.0 | 7.0 |
oo.DispatcherThird.goOut(int) | 1.0 | 4.0 | 4.0 |
oo.DispatcherThird.goOutEm(int) | 1.0 | 5.0 | 5.0 |
oo.DispatcherThird.into(ArrayList<arraylist>) | 1.0 | 2.0 | 2.0 |
oo.DispatcherThird.inWait() | 1.0 | 2.0 | 2.0 |
oo.DispatcherThird.run() | 4.0 | 5.0 | 5.0 |
oo.Elevator.close(int) | 1.0 | 2.0 | 2.0 |
oo.Elevator.Elevator(int,char) | 1.0 | 1.0 | 1.0 |
oo.Elevator.getFlag() | 1.0 | 1.0 | 1.0 |
oo.Elevator.move(int) | 1.0 | 2.0 | 2.0 |
oo.Elevator.open(int) | 1.0 | 3.0 | 3.0 |
oo.InputHandler.InputHandler(RequestList) | 1.0 | 1.0 | 1.0 |
oo.InputHandler.run() | 3.0 | 5.0 | 5.0 |
oo.Main.main(String[]) | 1.0 | 1.0 | 1.0 |
oo.RequestList.check(int,int,int) | 2.0 | 5.0 | 5.0 |
oo.RequestList.clear(ArrayList<arraylist>) | 1.0 | 3.0 | 3.0 |
oo.RequestList.firstAvailable(int,int) | 1.0 | 1.0 | 4.0 |
oo.RequestList.getOutFirst() | 4.0 | 5.0 | 7.0 |
oo.RequestList.getOutSecond() | 4.0 | 5.0 | 7.0 |
oo.RequestList.getOutThird() | 4.0 | 5.0 | 7.0 |
oo.RequestList.lookUpFirst(int,int,int,int) | 9.0 | 15.0 | 18.0 |
oo.RequestList.lookUpSecond(int,int,int,int) | 9.0 | 15.0 | 18.0 |
oo.RequestList.lookUpThird(int,int,int,int) | 9.0 | 15.0 | 18.0 |
oo.RequestList.max(int,int) | 2.0 | 1.0 | 2.0 |
oo.RequestList.min(int,int) | 2.0 | 1.0 | 2.0 |
oo.RequestList.ready(ArrayList<arraylist>) | 4.0 | 3.0 | 5.0 |
oo.RequestList.reset(int,int) | 1.0 | 10.0 | 10.0 |
oo.RequestList.returnAl(int,int,int,int,int) | 1.0 | 1.0 | 1.0 |
oo.RequestList.secondAvailable(int,int) | 1.0 | 1.0 | 8.0 |
oo.RequestList.separate(int,int,int) | 3.0 | 17.0 | 18.0 |
oo.RequestList.set(int) | 1.0 | 7.0 | 7.0 |
oo.RequestList.specialHandle(int,int,int) | 1.0 | 5.0 | 9.0 |
oo.RequestList.thirdAvailable(int,int) | 1.0 | 1.0 | 6.0 |
oo.SyOutput.initStartTimestamp() | 1.0 | 1.0 | 1.0 |
oo.SyOutput.println(String) | 1.0 | 1.0 | 1.0 |
Total | 123.0 | 236.0 | 287.0 |
class | OCavg | WMC |
oo.DispatcherFirst | 4.625 | 37.0 |
oo.DispatcherSecond | 4.625 | 37.0 |
oo.DispatcherThird | 4.625 | 37.0 |
oo.Elevator | 1.4 | 7.0 |
oo.InputHandler | 2.5 | 5.0 |
oo.Main | 1.0 | 1.0 |
oo.RequestList | 4.79 | 91.0 |
可以看出来,绝大多数的方法复杂度均较低,问题还是出现在共享对象的遍历上,这一点是我下一单元改进的重中之重。
bug:
这一次,我和其他同学共同搭建了测评机,用来自测和互测(也想当一回【假】狼人)。
但我这个废物测评机竟然在我手里没有发现任何bug(其他使用这架测评机的同学都或多或少地找到了自己和他人的bug,我好气啊~~)。
上周五与一位真·大佬聊天,他告诉我测评机的数据过于随机,可能找不到潜藏较深的bug,看来以后,我也需要读一读同组同学的代码,取长补短的同时,也可以顺便找找bug(究竟哪个是顺便?)。
注:特别鸣谢何岱岚同学,让菜鸡拥有了搭建测评机的可能。
电梯载人策略图示
SOLID自评
SPR(单一责任原则):几乎实现了一个类只负责一项工作,但其实也可以将这些类进一步细化,划分成更多的小类,每个类负责更细化的工作。
OCP(开放封闭原则):感觉还可以,但可拓展性尚需进一步加强。
LSP(里氏替换原则):没有用到继承(线程类除外)。
ISP(接口分离原则):没有用到接口。
DIP(依赖倒置原则):感觉做的不太好,构架的抽象程度不充分,仍然有面向过程的影子。
写在最后
心得与体会
多线程其实蛮有趣的(如果它们不是那么不听话的话),这也是我们将来走上工作岗位的重中之重,经过了两个单元,我也渐渐的适应了OO和OS的双重夹击(即天天熬夜); //为将来996做好了身体上的准备
虽然很累,但是真的学到了不少有用的知识和技能,提升了自己的核心竞争力; //离996岗位更进了一步
第三次作业有幸分到了A组,见到了许多大佬的代码,让我更加领会到了面向对象的含义和优秀的架构,今后我会继续学习大佬们的代码风格,争取可以活(xian)学(xue)活(xian)用(mai)。
虽然不知道下次作业是啥,但肯定和多线程脱不开干系,继续努(ao)力(ye)吧。
愿指引明路的苍蓝星为我们闪烁