OO第二单元作业总结
本单元作业为电梯调度的有关问题设计,让我们从单线程编程向多线程过渡。本单元的作业对我来说是一种全新的体验,特别是在多个线程同步控制方面,让我有了从无到有的学习与收获,而在测试多线程程序方面则让我感到十分艰难。总之,本单元的作业让我对多线程有了初步的认识和掌握,虽然作业得分不高,但是还是很有收获吧。
一、 从多线程的协同和同步控制方面,分析和总结自己三次作业的设计策略。
在这三次作业中,我都是使用一个请求队列作为共享队列,输入线程向请求队列里加入请求,而电梯从请求队列中获取请求进行运人的操作。
第一次作业:傻瓜式调度
傻瓜式调度电梯时,自己还没有搞懂多线程的基本的同步互斥的锁的问题,导致在这次作业中,我本意想Wait的地方(请求队列为空,则电梯线程就等待)根本就没有wait,当然,我想notify的时候也肯定没有真正notify,这就导致了电梯与请求队列之间进行了暴力轮询来进行信息的传递,然而第一次作业的CPU时间给的限度太大,也并没有检测出来这个问题。对于共享队列的保护,则是通过syncronized修饰共享队列访问和操作的方法进行保护(第一次作业还不是很明白,只知道无脑syncronized,不管需不需要锁都修饰上)。
第二次作业:可捎带式调度
第二次作业,我采用的就是跟指导书上规则差不太多的可捎带式调度规则,对电梯沿途可以捎带的人进行捎带。第二次作业由于限制了CPU时间,导致第一次作业出现的暴力轮询问题暴露了出来,解决策略就是通过单例模式,实现了电梯线程的wait和notify,同时还使用单例模式解决了线程退出的问题(通过public方法改变这一单例的成员变量来设置退出标志位)。
第三次作业:多部电梯
多部电梯作业,我基本没有任何优化,让三部电梯自由地去抢请求,佛系运人。由于多部电梯对象使用同一个电梯类进行创建,单例模式好像不太好办了,只能重新思考让线程wait和notify的方法,直到这次作业,我才真正明白(可能吧)了锁共享对象从而进行wait和notify操作,调用某个对象的wait()方法能让当前线程阻塞,并且当前线程必须拥有此对象的锁;调用某个对象的notify()方法能够唤醒一个正在等待这个对象的锁的线程。从而实现了多线程的协同和同步控制。不过这次作业有三个电梯,而且我的程序是在运行过程中后续向请求队列加入请求,因此程序的退出有了麻烦,最后没有想出合适的解决办法而采用了程序运行200s后退出,这也是一个很大的问题。
二、 基于度量来分析自己的程序架构
第一次作业:
第一次作业由于比较简单,各个类和方法的实现都不是很复杂,而且从类图可以清楚的看出Controller线程和InputThread线程对共享对象RequstQueue的调用关系,划分还是比较清晰的。
第二次作业:
第二次作业还是单电梯,本次作业的控制器还是跟一部电梯差不多,不过加入了Person类来增加电梯容纳的人数(第一次作业为单人,而本次作业有多人)并重设电梯到达楼层,从而完成捎带任务。不过由于增加了捎带功能,电梯需要判断能否捎带和重置to楼层,因此导致Controller类和方法有些复杂。
第三次作业:
第三次作业把Elevator类作为电梯模板,创建了三个电梯线程,并且由于捎带问题和电梯有的楼层到达不了,导致电梯类的实现很复杂,有些方法复杂度较高,并且所有的功能实现都在电梯类里完成,造成电梯类方法数较多而且比较冗杂。从类图方面看跟前两次作业相似,除了改之前作业中Controller这个虚假的电梯为真正的电梯类Elevator,其他基本没有变化。
基于SOLID原则的分析:
Single Responsibility Principle:基本遵循,在作业1和作业2中电梯和控制器有些重合,但也只是当前楼层和目标楼层这些成员变量的重合,方法方面还是不一样的,到了第三次作业就完全分离了,基本做到了一个类只负一种类型的责任。
Open Close Principle:没有较好体现。在进行第一次电梯设计时,没有仔细思考作业的设计结构,简简单单地想了一下就写了,致使可拓展性很差,到第二次作业时不得不进行重构,而第二次到第三次作业则基本遵循了开放封闭原则,不过还是对某些方法有一定的修改,主要还是进行了拓展。由此可以看出设计结构的重要性,好的设计结构应该是易扩展的,而不是用一次就得重构。
Liskov Substitution Principle:由于在这三次作业中都没有用到继承,所以没有体现里氏替换原则。
Interface Segregation Principle:由于在这三次作业中都没有用到接口,所以没有体现接口分离原则。
Dependency Inversion Principle:由于在这三次作业中都没有用到抽象接口,所以没有体现依赖倒置原则。
三、 分析自己程序的bug
第一次和第二次作业都是简单的按照指导书去写,在测试中没有发现bug。不过在第一次作业中,程序不能在输入结束(即^D)的时候正常退出,这是程序的一大问题,好在在第二次作业的时候解决了这个问题。
第三次作业自己的程序出了很严重的问题。首先是比较简单的电梯空载时(去接第一个人)的运行问题,由于自己程序的错误,导致在电梯从1楼去-1楼接人和从-1楼去1楼接人都会出现多跑过一层的现象,错误代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 for (floor = floor + 1; floor < from; floor++) { 2 if (floor == 0) { 3 floor++; 4 } 5 /*if (floor >= from) { 6 break; 7 }*/ 8 try { 9 Thread.sleep(runtime); 10 } catch (Exception e) { 11 ; 12 } 13 TimableOutput.println(String.format("ARRIVE-%d-%s", 14 this.floor, this.name)); 15 } 16 try { 17 Thread.sleep(runtime); 18 } catch (Exception e) { 19 ; 20 } 21 TimableOutput.println(String.format("ARRIVE-%d-%s",this.floor, this.name));
出现这种问题的原因还是对边界条件的疏忽:初始条件已经默认走了一层到了0层,满足限制条件,但是在代码内又将0层修改为了1层,由于缺乏再次判断,循环结束时又走了一层,导致走两层,从而出现错误。
另一个严重的错误出现在捎带过程中,由于电梯到达不了某些楼层,而我所写的电梯是让所有去不了自己的目的楼层的人都在本电梯最终的目的楼层出电梯(当时设计的时候没有仔细考虑捎带情况导致出现这种错误),导致人们出电梯的楼层,另外两个电梯可能到达不了,从而导致这些人永远也不去了自己的目的楼层。我能想到的比较简单的解决方法是让这些人在15楼或1楼出电梯,而三个电梯都可以到达这两个楼层,从而解决这个问题(虽然比较简单比较傻)。
四、 分析自己发现别人程序bug所采用的策略
说来惭愧,本单元的作业我都没有成功hack他人。多线程的测试相比较第一单元的单线程程序增加了不少难度和不确定性,很多数据结果在多线程中很难复现,这给寻找他人的bug提升了很大的难度。而且本单元的作业不同于第一单元,给定了输入,统一实现了格式的检查,让我们不能像第一单元作业一样从格式上找错,而只能从电梯运行的逻辑中寻找错误,这就需要阅读并理解大量代码。当然,也可以通过自动化测试来寻找他人的bug,我觉得这种方法用来检查自己的程序逻辑是否出错很有帮助,用来hack就显得有些以分数为目标了,当然如果自己可以写出或真的搞懂测试程序的话,那一定是对作业本身有着比较清楚的认识和理解,这样才是真正有意义的hack。
五、 心得体会
本单元作业给我最大的体会就是作业的初始设计与思考过程一定要仔细仔细再仔细,设计的时候要考虑周到,不要完成简单实现就觉得万事大吉,再分析一些其他情况,以免出现纰漏。就像我这次的第三次作业实现,由于初始设计时出电梯的愚蠢设计,导致程序出现重大bug,只能通过打补丁的方式对错误进行修补,否则就将部分重构,因此初始的设计一定要考虑全面,花费充分的时间构思全面了总比以后遇到错误就疯狂打补丁要好。
至于多线程的协同和同步控制方面,要十分清楚唤醒线程的时机,比如在第三次作业中,我的设计是人如果在非目的楼层出电梯时,就会新增一条请求并加入请求队列,如此这样就应该在其加入请求队列后唤醒其他线程,以免出现新的请求得不到处理的情况。多线程的线程安全需要注意细节,将细节处理好才能保障各线程的正常进行。