本文UML图省略了绝大多数方法,包括一些起辅助作用的方法(不是主要功能)。所有的属性都是private,为了简略没有打'-'符号。
第五次作业
只有一部电梯,人数不限。
本次作业我设计了两个线程,一个负责读输入(Read),一个负责模拟电梯轿厢的行为(Elevator)。这两个线程都修改Scheduler对象,Scheduler维护了等待队列。
设计时,我尽量避免Read和Elevator间访问,这样可以减少线程间协同出问题。唯一的问题是判断结束,我使用一个变量表示结束,使得在Elevator执行时只需要且只能读这个只能被Read修改的变量来判断自己何时结束。
我的Scheduler的方法全加了 synchronized 关键字,使得在涉及这个类的操作都变成了顺序的,就避免了复杂的同步控制。
下面是代码静态分析:
class OCavg WMC
Elevator | 2.230769230769231 | 29.0 |
Main | 1.0 | 1.0 |
Read | 1.5 | 6.0 |
Scheduler | 2.142857142857143 | 15.0 |
Total | 51.0 | |
Average | 2.04 | 12.75 |
method ev(G) iv(G) v(G)
Elevator.Arrive() | 2.0 | 4.0 | 5.0 |
Elevator.close() | 1.0 | 1.0 | 1.0 |
Elevator.down() | 1.0 | 1.0 | 1.0 |
Elevator.Elevator(Read) | 1.0 | 1.0 | 1.0 |
Elevator.getPassBy() | 2.0 | 2.0 | 3.0 |
Elevator.gotToTar() | 1.0 | 4.0 | 4.0 |
Elevator.in(PersonRequest) | 1.0 | 1.0 | 1.0 |
Elevator.isWorking() | 1.0 | 1.0 | 1.0 |
Elevator.open() | 1.0 | 1.0 | 1.0 |
Elevator.out(PersonRequest) | 1.0 | 1.0 | 1.0 |
Elevator.run() | 3.0 | 15.0 | 17.0 |
Elevator.setScd(Scheduler) | 1.0 | 1.0 | 1.0 |
Elevator.up() | 1.0 | 1.0 | 1.0 |
Main.main(String[]) | 1.0 | 1.0 | 1.0 |
Read.endInput() | 1.0 | 1.0 | 1.0 |
Read.run() | 3.0 | 4.0 | 4.0 |
Read.setEndInput(boolean) | 1.0 | 1.0 | 1.0 |
Read.setScd(Scheduler) | 1.0 | 1.0 | 1.0 |
Scheduler.add(PersonRequest) | 1.0 | 2.0 | 2.0 |
Scheduler.getBy(int,int) | 1.0 | 4.0 | 4.0 |
Scheduler.getRequest() | 2.0 | 1.0 | 2.0 |
Scheduler.length() | 1.0 | 1.0 | 1.0 |
Scheduler.rmpr(PersonRequest) | 2.0 | 1.0 | 2.0 |
Scheduler.setElv(Elevator) | 1.0 | 1.0 | 1.0 |
Scheduler.toRqFl(int) | 1.0 | 2.0 | 3.0 |
Total | 33.0 | 54.0 | 61.0 |
Average | 1.32 | 2.16 | 2.44 |
电梯线程的run()方法最复杂,所以它的圈复杂度比较高。我认为虽然它的设计复杂度高,这是因为解析方法不得不与很多方法联系紧密,难以避免。但是它的基本复杂度不高,所以最后也没出什么问题。总体看这次的设计不是很复杂,比较容易管理。
由于这次作业任务相对清晰且准确,我被强测测出了超时,Real Time Limit Exceed。原因是我的捎带规则过于严格,没有充分利用“乘客人数不限”这一条件,没有把在当前层能上电梯的都装进电梯。此外,我认为指导书上给出的作为时间性能判断标准的算法和评测机实际使用的不完全一致,因为我的算法严格优于指导书给的算法,却超时了。
我在阅读别人的代码后,发现有人在如何取请求的部分很混乱。于是我构造了同一时刻多条请求的样例,hack成功。
在检查线程安全方面,我先找到被不止一个线程访问的对象,然后找到所有修改这个对象的操作,观察这些操作的代码,尤其是没有保护的部分,自己想几种不同线程相关语句交叉的执行顺序,若有可能有问题的顺序就会有线程不安全。但是这种方法构造的样例要看运气才能hack成功,我没有靠这种方法找到的问题hack成功。
第六次作业
多部电梯,有人数限制。
本次作业和上一次的设计几乎完全相同。
线程间的协同依然只有一个Read线程和多个电梯线程,他们都能修改Scheduler类。能修改Scheduler类的方法只有Scheduler的方法,且都加上了 synchronized 关键字,这使得修改和访问Scheduler对象时不会产生现成安全问题,协同也没有什么会出问题的地方。
我的Read线程不依赖任何类,ELevator线程只依赖Scheduler类,仅仅在判断结束时会依赖Read类。Scheduler只依赖Read类。从这里可以看出来我的设计不会产生互相等待一类的情况。所有的类间关系都是单向的,就不会有死锁。
此次作业中,我对上次作业中较为杂乱的方法合并成几个执行完整功能的方法,为了便于本次的编写,但是损失了一些可扩展性。
我对算法进行了彻底改变。我把上一次“电梯有请求才动,只要有路过就捎带”的算法改为“电梯不停在最底层和最顶层折返,只带同向的乘客”,保证了效率。
class OCavg WMC
Elevator | 2.4545454545454546 | 27.0 |
Main | 3.0 | 3.0 |
Read | 1.4 | 7.0 |
Scheduler | 2.0 | 8.0 |
Total | 45.0 | |
Average | 2.142857142857143 | 11.25 |
method ev(G) iv(G) v(G)
Elevator.close() | 1.0 | 1.0 | 1.0 |
Elevator.down() | 1.0 | 1.0 | 2.0 |
Elevator.Elevator(char,Read) | 1.0 | 1.0 | 1.0 |
Elevator.goNext() | 1.0 | 9.0 | 11.0 |
Elevator.in(PersonRequest) | 1.0 | 1.0 | 1.0 |
Elevator.open() | 1.0 | 1.0 | 1.0 |
Elevator.out(PersonRequest) | 1.0 | 1.0 | 1.0 |
Elevator.run() | 1.0 | 7.0 | 7.0 |
Elevator.setScd(Scheduler) | 1.0 | 1.0 | 1.0 |
Elevator.toLeave() | 1.0 | 3.0 | 3.0 |
Elevator.up() | 1.0 | 1.0 | 2.0 |
Main.main(String[]) | 1.0 | 3.0 | 3.0 |
Read.endInput() | 1.0 | 1.0 | 1.0 |
Read.Read(ElevatorInput) | 1.0 | 1.0 | 1.0 |
Read.run() | 3.0 | 4.0 | 4.0 |
Read.setEndInput(boolean) | 1.0 | 1.0 | 1.0 |
Read.setScd(Scheduler) | 1.0 | 1.0 | 1.0 |
Scheduler.add(PersonRequest) | 1.0 | 1.0 | 1.0 |
Scheduler.length() | 1.0 | 1.0 | 1.0 |
Scheduler.setElv(ArrayList) | 1.0 | 1.0 | 1.0 |
Scheduler.toIn(int,boolean,int) | 3.0 | 6.0 | 7.0 |
Total | 25.0 | 47.0 | 52.0 |
Average | 1.1904761904761905 | 2.238095238095238 | 2.4761904761904763 |
从这里可以看出我对于电梯线程的run()方法中功能拆分比较成功,因为它的三项复杂度都比上一次作业小了很多。其次复杂的是新整合成的方法中的两个,分别为Scheduler.toIn()和Elevator.goNext()。这两个方法各自的分支数不多,所以圈复杂度也都不很高;设计复杂度也都比较低,所以最后没出什么问题。
我自己这次没有被测到bug。我也没测出来别人的bug。我的测试策略和上一次作业相同,但是没有结果。
我这次由于修改对象的方法被合并到很少,而且都只操作Scheduler这一个非线程的对象,使得我很容易根据可能的执行先后获同时的情况判断自己有没有现成安全问题。
我在看别人的代码时发现有的对象间互相修改,看似容易出死锁等问题,但是他处理的很好,基本类似于顺序执行,因此也没有什么问题。
第七次作业
可以在运行中增加新电梯。
本次作业我依然设计了两个线程,一个是Read,另一个是Elevator的子类。
我的线程安全设计:所有要加入等待队列ElvQue的必须先去分配器Scheduler被分配,而分配器的方法都有synchronized 关键字,分配到不同等待队列后电梯才可以取走请求。这样加和取请求都不会有线程安全问题。
为了防止出现时间先后相关的问题,我把每个需要换成的请求都当做一个请求送进电梯,电梯送到换乘站后,这个请求作为新请求加入等待队列,就不会出现“未到换乘站却先换成了”的问题。
我在用 synchronized 限制的时候,会考虑清楚执行这些代码的期间是否绝对不允许有别的涉及这个对象的操作。例如从等待队列取出上电梯,再删除等待队列中对应项,这是两个行为,这中间不能插入对等待队列的操作,否则会出现“上了两部电梯”的情况,因此要一起在 synchronized 中。若是“先从等待队列移除,再放进电梯”,那么放进电梯这部分就不用 synchronized 限制了,因为修改等待队列已不会产生问题。我的同步设计是基于以上这些的。
电梯线程的结束条件也进行了大幅修改,变为:输入结束 且 对应种类的等待队列为空 且 所有电梯都无乘客。这样可以避免电梯提前自己停工了,使得有人换成失败的情况。
这一次我设置了三个等待队列ElvQue,对应三类电梯,每类电梯只能在自己的等待队列里取,同类电梯共享一个队列。我把Scheduler重新赋予了职能:它成为了一个把新的乘客请求分配到这三个等待队列的分配器。
由于涉及的对象比较多,我每个电梯线程的结束条件都很复杂。
class OCavg WMC
Elevator | 1.35 | 27.0 |
ElvA | 9.666666666666666 | 29.0 |
ElvB | 5.666666666666667 | 17.0 |
ElvC | 5.666666666666667 | 17.0 |
ElvQue | 1.6 | 8.0 |
ElvQueA | 3.0 | 3.0 |
ElvQueB | 3.0 | 3.0 |
ElvQueC | 3.0 | 3.0 |
ExcutableRequest | 1.0 | 7.0 |
Main | 1.0 | 1.0 |
Read | 2.5 | 10.0 |
Scheduler | 3.5 | 14.0 |
Total | 139.0 | |
Average | 2.6226415094339623 | 11.583333333333334 |
method ev(G) iv(G) v(G)
Elevator.close() | 1.0 | 1.0 | 1.0 |
Elevator.curFl() | 1.0 | 1.0 | 1.0 |
Elevator.down() | 1.0 | 1.0 | 2.0 |
Elevator.Elevator(String,Read,ElvQue,Scheduler) | 1.0 | 1.0 | 1.0 |
Elevator.getElvQue() | 1.0 | 1.0 | 1.0 |
Elevator.getScd() | 1.0 | 1.0 | 1.0 |
Elevator.in(ExcutableRequest) | 1.0 | 1.0 | 1.0 |
Elevator.isOpen() | 1.0 | 1.0 | 1.0 |
Elevator.isUp() | 1.0 | 1.0 | 1.0 |
Elevator.open() | 1.0 | 1.0 | 1.0 |
Elevator.out(ExcutableRequest) | 1.0 | 2.0 | 2.0 |
Elevator.psng() | 1.0 | 1.0 | 1.0 |
Elevator.run() | 1.0 | 6.0 | 6.0 |
Elevator.setSize(int) | 1.0 | 1.0 | 1.0 |
Elevator.setTime(int) | 1.0 | 1.0 | 1.0 |
Elevator.setUp(boolean) | 1.0 | 1.0 | 1.0 |
Elevator.size() | 1.0 | 1.0 | 1.0 |
Elevator.toLeave() | 1.0 | 3.0 | 3.0 |
Elevator.up() | 1.0 | 1.0 | 2.0 |
Elevator.waitQue() | 1.0 | 1.0 | 1.0 |
ElvA.ElvA(String,Read,ElvQue,Scheduler) | 1.0 | 1.0 | 1.0 |
ElvA.goNext() | 1.0 | 23.0 | 27.0 |
ElvA.hasFloor(int) | 3.0 | 1.0 | 3.0 |
ElvB.ElvB(String,Read,ElvQue,Scheduler) | 1.0 | 1.0 | 1.0 |
ElvB.goNext() | 1.0 | 13.0 | 13.0 |
ElvB.hasFloor(int) | 3.0 | 1.0 | 4.0 |
ElvC.ElvC(String,Read,ElvQue,Scheduler) | 1.0 | 1.0 | 1.0 |
ElvC.goNext() | 1.0 | 13.0 | 13.0 |
ElvC.hasFloor(int) | 3.0 | 1.0 | 3.0 |
ElvQue.add(ExcutableRequest) | 1.0 | 1.0 | 1.0 |
ElvQue.getQueue() | 1.0 | 1.0 | 1.0 |
ElvQue.length() | 1.0 | 1.0 | 1.0 |
ElvQue.remove(ExcutableRequest) | 1.0 | 1.0 | 1.0 |
ElvQue.toIn(int,boolean,int) | 3.0 | 5.0 | 6.0 |
ElvQueA.hasFloor(int) | 3.0 | 1.0 | 3.0 |
ElvQueB.hasFloor(int) | 3.0 | 1.0 | 4.0 |
ElvQueC.hasFloor(int) | 3.0 | 1.0 | 3.0 |
ExcutableRequest.ExcutableRequest(PersonRequest) | 1.0 | 1.0 | 1.0 |
ExcutableRequest.from() | 1.0 | 1.0 | 1.0 |
ExcutableRequest.id() | 1.0 | 1.0 | 1.0 |
ExcutableRequest.setFrom(int) | 1.0 | 1.0 | 1.0 |
ExcutableRequest.setTo(int) | 1.0 | 1.0 | 1.0 |
ExcutableRequest.target() | 1.0 | 1.0 | 1.0 |
ExcutableRequest.to() | 1.0 | 1.0 | 1.0 |
Main.main(String[]) | 1.0 | 1.0 | 1.0 |
Read.endElvsandInput() | 3.0 | 2.0 | 3.0 |
Read.endInput() | 1.0 | 1.0 | 1.0 |
Read.run() | 5.0 | 6.0 | 7.0 |
Read.setEndInput(boolean) | 1.0 | 1.0 | 1.0 |
Scheduler.add(ExcutableRequest) | 5.0 | 14.0 | 14.0 |
Scheduler.EndScd() | 1.0 | 4.0 | 4.0 |
Scheduler.Scheduler(ElvQueA,ElvQueB,ElvQueC) | 1.0 | 1.0 | 1.0 |
Scheduler.setWorking(boolean) | 1.0 | 1.0 | 1.0 |
Total | 77.0 | 133.0 | 156.0 |
Average | 1.4528301886792452 | 2.509433962264151 | 2.943396226415094 |
我的ElvA类最不好,因为它的各项复杂度都最高。这主要是因为我针对A类电梯设计了稍微复杂的算法。在来回扫描的基础上,由于A的两段可上下区间离得非常远,因此在将要离开其中一个区间时,要判断是否需要离开,如果不需要就再在当前区间继续扫描,而非两个区间轮流扫描。条件比较多,导致圈复杂度提高了。
这一判断涉及了ElvA和ElvQue的一些方法,导致设计复杂度提高。
此外我的分配器Scheduler对象(这个名字起的很不好,有误导性)也有较高的圈复杂度,是因为要判断一个请求分配给哪一类电梯最合适,涉及复杂的条件判断。
我被测出的bug都是Real Time Limit Excced,因为我没能控制好A类电梯之间的线程安全的问题,就靠错开时间来解决,导致时间过长。
测试策略和前两次类似,只不过我着重考虑了线程安全的问题,我先找到被不止一个线程访问的对象,然后找到所有修改这个对象的操作,观察这些操作的代码,尤其是没有保护的部分,自己想几种不同线程相关语句交叉的执行顺序,若有可能有问题的顺序就会有线程不安全。但是这种方法构造的样例要看运气才能hack成功,我没有靠这种方法找到的问题hack成功。反倒是为了单纯测试读取并分配集中的请求这部分,我设置了同时的请求,有人出错。
我还重点阅读换乘策略,想找到时间先后不匹配和未到终点一类的错误,然而别的同学在这方便没错。
第七次作业可扩展性
我认为我的第七次作业不够完善,可扩展性有限。
SRP原则:每个类方法都只有一个明确的职责。我认为这一部分我做的相对较好。我有负责读取的类,负责分配的类,负责维护队列的类,还有电梯类。这是基于功能分配的类,因此它们各自有明确的职责。
OCP原则:无需修改已有实现(close),而是通过扩展来增加新功能(open)。我在这部分做的不好。第二次删除并合并了第一次的很多方法,第三次更是把负责维护队列的类的名字由Scheduler换成了ElvQue,却把负责分配的类命名为了Scheduler。这样很不好。我觉得自己比较符合此原则的地方是我的电梯类在后两次作业中几乎没有进行修改,这说明我的电梯类设计的符合此原则,只是控制的类比较乱。
LSP原则:任何父类出现的地方都可以使用子类来代替,并不会导致使用相应类的程序出现错误。我在此次作业中所有父类都是抽象类,不存在使用父类的问题。
ISP原则:类间的依赖关系应该建立在最小的接口上。我这个做的不好,因为我把抽象类都是尽量包含所有共有的属性和方法,而非调用者需要的最少的方法。我认为我可以单独做一个调用者需要的接口,让抽象类实现这个接口,相当于多分出来一个接口。这对某次任务会麻烦一些,但是对扩展很友好。
DIP原则:这次的继承不深,不太涉及这个原则。
心得体会
我在这单元收获很大。这是我第一次编多线程的程序,感觉自己没有进行特别细致的拆分,常常是给整个方法加了锁。我日后要改这个问题,尤其是当请求并发量大的时候,每一点时间都非常珍贵,要尽可能地用于处理。
线程安全问题:由于我对一个方法内部分代码执行同时或先后的问题分析的不是很彻底,我就采用了“如果加 synchronized 不会导致死锁且不会明显的降低效率,我就在方法上加这个限制”的策略。这有点偷懒,但是不容易出错。到了最后一次作业,由于这个策略几乎不满足(因为有相互的调用,方法上加 synchronized 会死锁),就改为“尽量小范围的加 synchronized 限制”这一策略,是为了极力避免死锁。总之是那种问题在当前次作业难以避免我就以避免这一种问题为出发点的策略。我认为这样以后很可能顾此失彼,还要练习更细致地分析,然后加锁才行。
设计原则:本轮作业我比上一轮作业更加注重设计原则,这使得我的代码在迭代中进行的修改小了很多,也没有大规模重构的发生。我从这种比较里认识到了“高内聚、低耦合”的意义所在。再加上确保线程安全要花大量精力,依靠设计原则减少自己无谓的思考和乱设计非常重要。我认为若我不知道SRP原则,我肯定无法完成本轮作业。我以后要更加注意OCP原则,而不是因为某次是最后一次作业就不管了。
我总是在一些设计方案上反复纠结,开始写代码过晚,导致最后出结果很仓促,不能很充分地测试。