第二单元总结
概述
本单元主要考察多线程的运用,其中包括对于线程同步、线程安全的理解以及wait()
、notify()
机制的运用和生产者消费者模型的实现。同时还需考虑如何设计调度的架构,调度的算法、如何捎带以及电梯的约束等等。可以说本单元从第一次作业开始便具有相当的难度,第二次、第三次更是层层递进,如果每次都有相当程度的重构,不留出相当的时间编码,想要完成堪比登天。
- 第一次:单部多线程可捎带电梯
- 第二次:多部相同类型多线程可捎带调度电梯
- 第三次:多部不同类型多线程可捎带调度电梯
多线程的协同和同步控制
对于多线程的协同与同步,我可以说是三次作业结束老师讲评之后才算真正理解的。我从来没有真正将锁的位置完全放“对”过。虽然知道使用生产者消费者模型,要将“托盘”——共享对象加锁,但是我依旧在线程代码里面加了很多锁 ,各种互相判断如果内部队列为空就要阻塞、如果输入null且队列为空就System.exit(0)
。这样带来了大量的线程安全问题,debug时给我造成了相当巨大的难度。所以任何需要在线程之间交互的变量都应该集成内聚为共享变量类:总的调度队列,内部的调度队列以及判断输入结束的指针(退出循环的条件)。电梯内部的队列有requests
电梯被分到的需求队列和innerPassengers
电梯内乘客的队列两种,而requests
可能被其他的线程修改,所以应该改为共享变量。而如何判断终止程序跳出循环是我时间截止前一直没有解决的问题。我将在bug修复中详细介绍。
功能设计与性能设计的平衡
这是我做得非常差的地方,可能天生小脑有问题,平衡性我从来权衡不来(T_T),它是我后两次作业无效的主要原因之一。第一次我采用LOOK算法,这是一种非实时的、性能方差较小而均值较大的算法,采用这种算法使得我几乎没有得任何性能分。第二次我打算改进算法,采用了单个电梯用LOOK调度并以运送完乘客的总时间最短为标准的需求分配的方式,奈何有太多线程安全的bug,到最后也没有改完,我最后甚至连一次也没有提交过。第三次作业,为了避免翻车,在外层需求分配时我没有采用任何算法,尽量从简,只需要请求满足能接上或者能送达的一些约束条件就行,内层仍然采用LOOK算法。最后没想到还是在结束程序的判断上翻了车,最后又失效了,不过总算是提交过,可以Bug修复(虽然不知道分数上有没有任何变化)。
至于为什么不愿意再尝试更复杂(最短路+图)的优化算法,其一是因为复杂的优化可能超出自己的掌控范围,出于本人是写bug高手的考虑,加之自己对于多线程的理解一直存在困扰,为了减少线程安全问题,不打算在优化上花费过多精力,因为可能反而费力不讨好,最终结果也不一定更好;其二便是看到有的同学仅采用某种静态的取模的方式随机分配便能获得相当好的性能分,这说明只要保证一定的平均公平的原则就能在大多数情况下获得较优解。
在SOLID设计原则中:
SRP原则在elevator
类中体现较差,本来内部调度单独采用了一个Scheduler
类,但由于look算法本身比较简单,本身不需要太多调度的算法,更多是直接执行,所以最后将Scheduler
类的方法集成在elevator
类中了。导致此方法比较庞大。但总体来讲除此之外的部分划分地比较好,做到了各施其职。
OCP原则在三次作业中难以实现,每次的需求变化总是使得我上次构思的调度方法失效,使得我不得不重构大部分调度和分配请求的算法,这说明在第一次作业便构思出具备足够拓展性的架构的重要性,在这方面我还应该多多思考。
由于本单元未采用父类继承和接口实现所以没有LSP、ISP和DIP原则的体现。
基于度量的程序结构分析
本部分以第三次作业为例,列出了类的属性个数、方法个数、每个方法规模、每个方法的控制分支数目、类总代码规模以及UML类图等等程序结构属性。
代码行数分析:
Source File | Total Lines | Source Code Lines |
---|---|---|
AssignedQueue.java | 35 | 30 |
Direction.java | 12 | 10 |
Elevator.java | 289 | 271 |
ElevatorFactory.java | 26 | 23 |
ElevatorType.java | 102 | 94 |
Ending.java | 15 | 12 |
Input.java | 36 | 33 |
MainClass.java | 13 | 12 |
Passenger.java | 111 | 92 |
TotalScheduler.java | 107 | 102 |
UnassignedQueue.java | 42 | 36 |
Total | 788 | 715 |
性能度量:
Complexity metrics | 周五 | 17 4月 2020 12:10:51 CST | |
---|---|---|---|
Method | ev(G) | iv(G) | v(G) |
AssignedQueue.getPassengers() | 1 | 2 | 3 |
AssignedQueue.isEmpty() | 2 | 2 | 3 |
AssignedQueue.putPassenger(Passenger) | 1 | 2 | 2 |
AssignedQueue.removePassenger(Passenger) | 1 | 1 | 1 |
Direction.Direction(int) | 1 | 1 | 1 |
Direction.getDirection() | 1 | 1 | 1 |
Elevator.Elevator() | 1 | 1 | 1 |
Elevator.Elevator(String,ElevatorType,TotalScheduler,UnassignedQueue,Ending) | 1 | 1 | 1 |
Elevator.addPassenger(Passenger) | 1 | 2 | 2 |
Elevator.close() | 1 | 1 | 1 |
Elevator.getElevatorType() | 1 | 1 | 1 |
Elevator.getFloorAndCeil(TreeMap |
1 | 1 | 1 |
Elevator.getNowFloor() | 1 | 1 | 1 |
Elevator.getTheNearest(TreeMap |
3 | 6 | 8 |
Elevator.isFullLoad() | 2 | 1 | 2 |
Elevator.moveTo(int) | 7 | 7 | 7 |
Elevator.oneDirectTrip(TreeMap |
13 | 13 | 18 |
Elevator.open() | 1 | 1 | 1 |
Elevator.passengersIn() | 2 | 6 | 7 |
Elevator.passengersOut() | 2 | 3 | 4 |
Elevator.run() | 1 | 5 | 6 |
Elevator.trip(TreeMap |
1 | 4 | 4 |
ElevatorFactory.ElevatorFactory(TotalScheduler,UnassignedQueue,Ending) | 1 | 1 | 1 |
ElevatorFactory.getElevator(ElevatorRequest) | 3 | 3 | 3 |
ElevatorType.ElevatorType(int[],int,int) | 1 | 1 | 1 |
ElevatorType.beneficialTrip(ElevatorType,int,int) | 5 | 6 | 9 |
ElevatorType.commonFloors(ElevatorType,ElevatorType) | 1 | 1 | 9 |
ElevatorType.containsFloor(ElevatorType,int) | 3 | 1 | 3 |
ElevatorType.findTheNearestFloor(ElevatorType,Passenger) | 2 | 7 | 7 |
ElevatorType.getFloorsOfType() | 1 | 1 | 1 |
ElevatorType.getLoading() | 1 | 1 | 1 |
ElevatorType.getSpeed() | 1 | 1 | 1 |
Ending.addCounter() | 1 | 1 | 1 |
Ending.equalsZero() | 1 | 1 | 1 |
Ending.minusCounter() | 1 | 1 | 1 |
Input.Input(UnassignedQueue,TotalScheduler,Ending) | 1 | 1 | 1 |
Input.run() | 3 | 5 | 5 |
MainClass.main(String[]) | 1 | 1 | 1 |
Passenger.Passenger(PersonRequest) | 1 | 3 | 3 |
Passenger.Passenger(int,int,int,int) | 1 | 1 | 1 |
Passenger.getDestination() | 1 | 1 | 1 |
Passenger.getDirection() | 1 | 1 | 1 |
Passenger.getGetIn() | 1 | 1 | 1 |
Passenger.getHadChanged() | 1 | 1 | 1 |
Passenger.getHasArrived() | 1 | 1 | 1 |
Passenger.getId() | 1 | 1 | 1 |
Passenger.getInterchange() | 1 | 1 | 1 |
Passenger.getOutset() | 1 | 1 | 1 |
Passenger.outputIn() | 1 | 1 | 1 |
Passenger.outputIntoInterchanged() | 1 | 1 | 1 |
Passenger.outputOut() | 1 | 1 | 1 |
Passenger.outputOutInterchange() | 1 | 1 | 1 |
Passenger.setDirectionAfterInterChange() | 1 | 3 | 3 |
Passenger.setDirectionBeforeInterChange() | 1 | 3 | 3 |
Passenger.setInterchange(int) | 1 | 1 | 1 |
Scheduler.Scheduler(Elevator) | 1 | 1 | 1 |
Scheduler.getNextPassenger() | 1 | 6 | 6 |
Scheduler.getSameDirection(Passenger) | 1 | 8 | 8 |
TotalScheduler.TotalScheduler(UnassignedQueue,Ending) | 1 | 1 | 1 |
TotalScheduler.addElevator(Elevator) | 1 | 1 | 1 |
TotalScheduler.dispatch(int,ArrayList ) | 11 | 5 | 12 |
TotalScheduler.run() | 5 | 10 | 11 |
UnassignedQueue.getPassenger(Integer) | 1 | 2 | 3 |
UnassignedQueue.isEmpty() | 1 | 1 | 1 |
UnassignedQueue.putPassenger(Passenger) | 1 | 1 | 1 |
UnassignedQueue.readQueue() | 1 | 2 | 3 |
UnassignedQueue.setElevators(ArrayList ) | 1 | 1 | 1 |
Class | OCavg | WMC | |
AssignedQueue | 1.75 | 7 | |
Direction | 1 | 2 | |
Elevator | 3.75 | 60 | |
ElevatorFactory | 2 | 4 | |
ElevatorType | 3 | 24 | |
Ending | 1 | 3 | |
Input | 3 | 6 | |
MainClass | 1 | 1 | |
Passenger | 1.35 | 23 | |
Scheduler | 3.67 | 11 | |
TotalScheduler | 5.5 | 22 | |
UnassignedQueue | 1.4 | 7 | |
Module | v(G)avg | v(G)tot | |
Homework3 | 2.9 | 194 |
UML图:
本次作业我建立了Input
类(线程)来读入请求作为生产者,并放入托盘(共享队列)UnassignedQueue
,请求分配器TotalScheduler
作为消费者取出Passenger
并分配给满足可以有效运送乘客条件的电梯。如若要新建电梯,我采用了简单工厂模式ElevatorType
来处理需求,对于电梯类型,我规定了ElevatorType
这个枚举类,每个电梯具有电梯种类属性,其中封装了速度和载客量的参数,同时具有许多关于电梯换乘的方法。电梯具有内部队列和Ending
(作为停止运行的判断条件的计数器)作为电梯间的共享变量,当电梯送完一个需要转乘的乘客后,再次将乘客放回外部队列,等待其他电梯取走。
总调度器时序图:
内部时序图:
Bug分析与修复
第三次出现了输出不完整的错误,发现是在用命令行运行程序时判断输入结束后程序便自动结束了,可能是一些线程相关问题,具体感觉十分玄学,因为在console栏里面输入是无法复现的,只有在自动测试工具里面可以体现。
于是我修改了退出run()
函数的判断方法,建立了新的计数器类Ending
,当电梯内部队列新增一个请求时,Ending.counter
自增1,当电梯内部队列去除一个请求时,Ending.counter
自减1,所有的电梯共享一个Ending
对象。当input
读入null
且外部队列和Ending.counter
都为0时,在(最后结束运行的)某一电梯里执行System.exit(0)
操作退出程序。
同时修改了部分外部调度器分配请求的判断条件,归并简化了条件并单独构成dispatch()
方法。修改了判断“乘坐此电梯不会离终点更远”的beneficialTrip()
方法使其对从三楼到四楼这种无法找到能直接缩短起点与终点距离的请求无效,这样请求就能得到解决。
众所周知,互斥条件、保持和请求条件、 不剥夺条件、环路等待条件是(资源)死锁发生的四个必要条件,在本次作业中我的程序没有滋生死锁的环境,倒是很可能让线程饿死,尤其是在debug前,我将电梯退出run()
方法的判断放在了while循环里,线程进入while循环后在读取队列时如果队列为空则等待,而此时如果读入null,不唤醒电梯的话,则会让电梯永远也无法退出,只能在外面强行exit,在第一次作业是适用的,到了第三次便不适用了,因为后面如果换乘可能还会再次用到此类型的电梯,所以应该让其成为死循环并当读入null时唤醒线程设置readNull
属性为真,当Ending.counter
为0时可以退出。
找Bug与Debug策略
由于三次都没有主动找或者没有机会找别人的bug( 泪奔/(ㄒoㄒ)/~~),所以我主要讲讲我如何找自己的bug。
我用python完成了一个定时输入程序,可以向打包好的jar输入完整格式的请求。并且编写了判断结果正确性的程序,主要用是否将乘客送到终点、是否无中生有、是否电梯吃人、乘客是否穿墙等条件判断。
在debug时,由于调试和在IDEA里面运行都效率极低且无法复现出问题,我逐渐领悟出了在代码间插入输出,然后用自动化测试工具运行的方法,效率大大提升。
我的心得体会
在三次作业中,我竟然就有两次无效,在无尽的悔恨当中,我试图找到原因。归根结底还是我对于课程重视不够、难度把握不足、在家学习低效、怠惰,对于多线程的基本概念是是而非,没有真正搞懂。本单元的作业给我好好上了一课,别人都觉得第一次作业难度大,后两次难度爬升小,为啥我后两次都没完成呢?纵观我完成作业的状态,周四才开始写OO作业,本身对课堂内容又没有复习,对于不懂的地方在网上查资料又觉得不耐烦,更不用说拓展。不过时至今日,我总算基本掌握、理解了多线程的课内知识,并且对于设计模式与原则的体会更加深刻。OO是一门有趣而困难的学科,如果不认真对待它,它也不会给人好脸色,但如果用心专研,一定能在其中找到十足的乐趣,这是一门应用性很高的学科。希望在今后的日子里,我能与OO和谐快乐共处!(不过我现在看到电梯就会胃疼,已然患上电梯调度PTSD,希望我能早日康复,走出阴霾(;´༎ຶД༎ຶ`))