OO第二单元作业主题为多线程,我之前从未接触过多线程知识。虽然听了理论课,但在完成第一单元作业时对多线程的一些概念仍然是一知半解,在网上补充了很多知识后才敢开始写,动手的时候也一直担心还会不会自己的理解有问题而出bug。总的来说,多线程编程是一种全新的体验,有一种学习数学时从平面几何跨越到立体几何的感觉。新概念会有些多,但在理清楚概念后多线程编程会非常有趣。
吸取第一单元作业的教训,在刚刚拿到第二单元第一次作业时,我就先穷尽想象力思考之后可能出现的新需求,多部电梯,人数上限,限制楼层,以及其他种种之后没有出现的需求都被我考虑到了。为了能够方便的实现上述新的需求,并且能够方便的扩展这些需求,我在设计第一次作业时花费了比较多的时间,但我终于还是很好地完成了程序的扩展性:我的三次作业的多线程控制部分几乎是相同的,仅仅在第三次作业时进行了微调,并且我在迭代设计时非常方便,添加新功能也非常快,没有进行过重构。算上设计,实现,提升性能,debug等的总时间,第一次作业总共花费大约8小时,而第二次作业仅仅花了大约4小时,第三次作业也只花费了大约5小时。最终分别拿到96.4,99.1,99.9的成绩,比上次有进步,总得来说对本单元作业我还是比较满意的。具体分析如下。
一、 多线程设计与性能设计分析
前文已经提过,我三次的多线程控制是几乎相同的。程序总共有3个类,MainClass,Scheduler(继承Thread),Elevator(继承Thread)。其中MainClass线程分别创建1个Scheduler对象和1个Elevator对象并分别启动线程。在MainClass中维护一个RequestList,并在创建Scheduler对象时作为其成员传入,在MainClass中读入请求并添加到这个List,每当电梯接受一个请求,Scheduler就从这个List中去除这个请求。由于其为共享对象,因此分别在MainClass和Scheduler中通过synchronizedd(RequestList)的方式保证安全性,以上便是读入部分。
接下来便是分析Scheduler和Elevator的交互。需要注意的是,考虑到调度时的方便性,Elevator类的实际意义是电梯组,即我将若干个电梯视为一个组。Elevator类中有数个List来分别记录电梯对应的楼层、人数、状态等等属性,List的长度即是总共的电梯数。当添加电梯时只需要像组中add即可。并且与很多同学的Scheduler分配请求至单个电梯,单个电梯根据自己被分配到的请求决定行动的模式不同,我的电梯组比较“笨”,没有自我判断的功能,全部交由Scheduler判断行动,Scheduler在每个行动周期(取决于不同的电梯type)修改Elevator对象的一个指令list,每个电梯读取这个指令list来完成行动。这样的好处在于我的调度算法的考虑更加全局化,电梯的行动并不是独立的,在调度上能够体现出整体性,Scheduler在分配请求时能更方便地考量所有电梯的状态,做出的判断更具全局性,在优化调度算法时比较方便。比如在第二次、第三次作业中,当Scheduler检测到有电梯即将满员,即空位较少时,能够通知其他较为空闲的电梯在不延误自身行程的情况下朝着该电梯方向靠拢,在自动化测试中发现这一优化提升算法不小的性能。
由于Scheduler每个行动周期都需要读取电梯组的信息以判断下一周期的指令,电梯组也需要读取指令来行动,因此Elevator对象为共享对象,通过synchronized(elevator)来达成线程安全。上述设计的另外一个好处就在于实现逻辑简单和debug容易,因为除去MainClass的线程外仅仅有两个线程,而假如每个电梯都设置一个线程的话由于线程过多,debug的时候也会不方便。
在功能与性能分析上,本单元作业的性能主要取决于调度算法,由前文所述,因为可以在每一个周期都根据全局所有状况来判断下一步的行动,所以该构筑在实现调度算法时是比较方便的,并且在优化时也能够进行前瞻性的预测行为来提高对于随机样例的总体性能。第一次作业采用最简单的look算法,即只要往原方向运行有收益:即该方向上有人可接或者有人可送则继续运行,否则返回,由于大意了(在自动化测试时觉得性能已经不错了),没有进行其他优化,最终只拿到96.4。第二次作业在之前的look算法的基础上加入了前瞻性,即前文所述在电梯快满时调度空闲电梯往该方向靠拢的算法,以及根据电梯组总体分布来使得电梯分布均匀的算法(例如尽量不使得过多的电梯处于高层),拿到了99.1分。第三次作业仍沿用第二次作业算法,将1和15层作为换乘层,未加入新的优化算法,拿到99.9分。由于最初构筑时认真考虑到了性能优化,本单元作业并没有在性能与构筑实现上感受到太大矛盾与难点。
最后是对可拓展性的分析,参考了微信群中大家讨论的各种脑洞后,对于临时删除电梯以及临时更改电梯请求的拓展,我的构筑都能够比较轻松地实现,前者只需要修改电梯组的成员变量,后者则更改对于电梯内部的请求即可,由于调度器是每个周期判断一次方向,在性能上也没有损失。其他还考虑过请求的优先级的设置,即优先级高的请求优先完成,对于这一扩展,由于调度器的调度是基于全局的情况,算法也比较好修改,也能较好地实现。而对于电梯开门关门时间和运行时间的改变上,只需要修改每次判断所经过的单位周期数即可。综上,我认为我的构筑能够实现大部分的扩展。
二、 程序结构分析
(1)第一次作业
类图与MetricsReloaded分析如下:
Elevator.Elevator() | 1.0 | 1.0 | 1.0 |
Elevator.getFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getRequests() | 1.0 | 1.0 | 1.0 |
Elevator.getStatus() | 1.0 | 1.0 | 1.0 |
Elevator.move() | 2.0 | 5.0 | 5.0 |
Elevator.run() | 1.0 | 6.0 | 7.0 |
Elevator.setMove(Boolean) | 1.0 | 1.0 | 1.0 |
Elevator.setStatus(int) | 1.0 | 1.0 | 1.0 |
MainClass.getBeginJudge() | 1.0 | 1.0 | 1.0 |
MainClass.getEndJudge() | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 3.0 | 6.0 | 6.0 |
Scheduler.dirctJudge(Elevator) | 16.0 | 11.0 | 17.0 |
Scheduler.getEndJudge() | 1.0 | 1.0 | 1.0 |
Scheduler.inOutJudge(Elevator) | 2.0 | 8.0 | 8.0 |
Scheduler.look() | 1.0 | 6.0 | 7.0 |
Scheduler.run() | 3.0 | 6.0 | 6.0 |
Scheduler.Scheduler(Elevator,List) | 1.0 | 1.0 | 1.0 |
Scheduler.show() | 1.0 | 2.0 | 2.0 |
Total | 107.0 | 128.0 | 149.0 |
Average | 1.5285714285714285 | 1.8285714285714285 | 2.1285714285714286 |
前文已述,调度算法主要集中于Scheduler类,Elevator类实际上只起着数据记录的作业。第一次作业中复杂度集中于dirctJudge方法,用于分析判断电梯在下个周期应当运行的方向。
(2)第二次作业
Elevator.Elevator(int) | 1.0 | 1.0 | 5.0 |
Elevator.getBeginJudge() | 1.0 | 1.0 | 1.0 |
Elevator.getFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getNum() | 1.0 | 1.0 | 1.0 |
Elevator.getRequests() | 1.0 | 1.0 | 1.0 |
Elevator.getStatus() | 1.0 | 1.0 | 1.0 |
Elevator.move() | 1.0 | 5.0 | 7.0 |
Elevator.run() | 1.0 | 6.0 | 7.0 |
Elevator.setBeginJudge(int) | 1.0 | 1.0 | 1.0 |
Elevator.setMove(int,Boolean) | 1.0 | 1.0 | 1.0 |
Elevator.setStatus(int[]) | 1.0 | 1.0 | 1.0 |
MainClass.getBeginJudge() | 1.0 | 1.0 | 1.0 |
MainClass.getEndJudge() | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 3.0 | 6.0 | 6.0 |
Scheduler.dirctJudge(Elevator) | 5.0 | 9.0 | 14.0 |
Scheduler.downJudge(int) | 13.0 | 15.0 | 24.0 |
Scheduler.getEndJudge() | 1.0 | 1.0 | 1.0 |
Scheduler.inOutJudge(Elevator) | 1.0 | 10.0 | 10.0 |
Scheduler.look() | 1.0 | 5.0 | 6.0 |
Scheduler.run() | 3.0 | 6.0 | 6.0 |
Scheduler.Scheduler(Elevator,List) | 1.0 | 1.0 | 1.0 |
Scheduler.show() | 1.0 | 2.0 | 2.0 |
Scheduler.upJudge(int) | 13.0 | 15.0 | 24.0 |
Total | 126.0 | 164.0 | 210.0 |
Average | 1.6578947368421053 | 2.1578947368421053 | 2.763157894736842 |
第二次作业中dirctJudge的方法复杂度分配给了upJudge和downJudge方法,这两个方法用于分析向上或者向下是否仍有价值,并在dirctJudge中对比两个方法的结果来得到下一步的最佳方向。程序的整体复杂度相对第一次稍有上升。
(3)第三次作业
Elevator.addNum() | 1.0 | 1.0 | 1.0 |
Elevator.Elevator() | 1.0 | 1.0 | 6.0 |
Elevator.getBeginJudge() | 1.0 | 1.0 | 1.0 |
Elevator.getCount() | 1.0 | 1.0 | 1.0 |
Elevator.getEleId() | 1.0 | 1.0 | 1.0 |
Elevator.getFloor() | 1.0 | 1.0 | 1.0 |
Elevator.getNum() | 1.0 | 1.0 | 1.0 |
Elevator.getRequests() | 1.0 | 1.0 | 1.0 |
Elevator.getStatus() | 1.0 | 1.0 | 1.0 |
Elevator.getType() | 1.0 | 1.0 | 1.0 |
Elevator.move() | 1.0 | 7.0 | 9.0 |
Elevator.run() | 1.0 | 6.0 | 7.0 |
Elevator.setBeginJudge(int) | 1.0 | 1.0 | 1.0 |
Elevator.setCount(int,int) | 1.0 | 1.0 | 1.0 |
Elevator.setEleId(int,String) | 1.0 | 1.0 | 1.0 |
Elevator.setMove(int,Boolean) | 1.0 | 1.0 | 1.0 |
Elevator.setStatus(int[]) | 1.0 | 1.0 | 1.0 |
Elevator.setType(int,int) | 1.0 | 1.0 | 1.0 |
MainClass.getBeginJudge() | 1.0 | 1.0 | 1.0 |
MainClass.getEndJudge() | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 3.0 | 6.0 | 6.0 |
Scheduler.aJudge(int) | 2.0 | 1.0 | 3.0 |
Scheduler.bJudge(int) | 2.0 | 1.0 | 4.0 |
Scheduler.cJudge(int) | 2.0 | 1.0 | 4.0 |
Scheduler.dirctJudge(Elevator) | 7.0 | 14.0 | 16.0 |
Scheduler.downJudge(int) | 16.0 | 18.0 | 27.0 |
Scheduler.exchangeJudge(int) | 2.0 | 1.0 | 3.0 |
Scheduler.getEndJudge() | 1.0 | 1.0 | 1.0 |
Scheduler.inOutJudge(Elevator) | 4.0 | 15.0 | 17.0 |
Scheduler.look() | 1.0 | 10.0 | 11.0 |
Scheduler.oneTurnJudge(PersonRequest) | 4.0 | 4.0 | 7.0 |
Scheduler.run() | 3.0 | 6.0 | 6.0 |
Scheduler.Scheduler(Elevator,List) | 1.0 | 1.0 | 1.0 |
Scheduler.show() | 1.0 | 2.0 | 2.0 |
Scheduler.typeFloorJudge(int,int) | 4.0 | 4.0 | 4.0 |
Scheduler.upJudge(int) | 16.0 | 18.0 | 27.0 |
Total | 169.0 | 215.0 | 273.0 |
Average | 1.7604166666666667 | 2.2395833333333335 | 2.84375 |
第三次作业中除了在第二次作业的基础上加入了换乘之外,调度算法整体变化较小,因此整体复杂度相对第二次仅有较小上升,仍然集中于dirctJudge。
三、程序Bug分析
在强测,我的三次作业均未检测出任何Bug。在互测中,第三次作业被hack出一个bug,该bug为纯手误bug(将一个数字打错了),由于自动化测试不够而没有检测出来。第一次作业在测试中出现了一些多线程逻辑上的bug。第二次作业和第三次作业由于没有修改多线程结构,而仅仅添加了调度算法,因此出现的bug也只可能有算法的手误bug和逻辑bug(不过该单元中我并没有写出逻辑bug)。因此具有参考意义的只有第一次作业中的多线程bug,原因为我对于多线程的概念理解不正确,如下:
(1)wait机制理解不正确
由于对wait的线程被唤醒时,是优先参加竞争还是与未被wait只是被阻塞的线程一起参加竞争理解错误而导致的死锁bug。实际上应该是同等地参与竞争。
(2)线程停止的bug
在写整个线程停止的过程中出现了几个bug,主要原因仍然是对于wait与唤醒的机制运用不成熟而导致的逻辑问题。会导致程序中某一个线程不断运行,其他终止。或者一个线程wait而其他终止。最终采用的方式是设置endJudge变量,让run方法自然跑到尾。
四、互测他人Bug方式分析
在互测中,我实现了同时对七位Room友的自动化测试。但由于大家都太强了,或者是我的自动生成数据写得不够具有针对性,在三次作业中都没有收获。
另外,我认为此次作业的bug主要集中于调度算法和多线程结构上。在阅读他人代码时主要集中于调度算法的方向判断和多线程的终止上,这两点很容易出错。在第二次作业中找到了终止bug从而hack成功。
还采用了IDEA提供的CoverUp功能,在运行过程中分析没有运行到的代码段,观察这部分代码段,更容易发现问题,再设计数据运行到这段代码就能找出Bug。
五、总结与反思
在这三次作业中我学习到了多线程的知识和许多调度算法。主要经验收获如下:
1. 面对新知识体系(例如多线程)应该透彻地理解概念之后再动笔,不然造成设计上的失误或者实现上的bug会浪费自己大量的时间。
2. 在选取算法的时候注意关注系统的整体性,由于这单元作业是随机化测试,应当借助大量的自动化测试来判断某一优化算法是否应该添加。
3. 延展性设计非常重要,这次我尝到了甜头,第二次第三次作业仅仅花了较少的时间就拿到了不错的成绩,希望下次再接再厉。
在设计,优化过程中听取了很多同学和助教的意见,非常感谢他们。