第二单元的主要任务是掌握面向多线程编程的方法,笔者通过这三次电梯作业,大概掌握了多线程编程的方法。现对每次作业进行总结。

第一次作业

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

Task:本次作业的任务是完成单部多线程傻瓜调度(FAFS)电梯的模拟。

设计策略:此次作业我一共写了5个类,其中ElevatorInputHandler是线程。调度器(Dispatcher)采用单例模式,内设一个私有变量requestArrayList,其主要功能相当于一个托盘,生产者(InputHandler)接收到一个请求,就调用DispatcherputRequest方法,将请求放入请求队列requestArrayListElevator可以通过getRequest方法从requestArrayList队列中取出一个请求并执行。StopElevator也是一个单例模式,当InputHandler监测到输入为null时,StopElevator中的变量stopFlag变为trueElevator停止工作的条件是stopFlag变为truerequestArrayList为空。

 

类图如下

OO电梯单元总结 随笔 第1张

 

方法和类的复杂度分析:

OO电梯单元总结 随笔 第2张

OO电梯单元总结 随笔 第3张

优点:从类图和方法以及类之间的内聚情况来看,此次作业笔者的思路还是比较清晰的。各类之间分工明确,只做好自己该做的事情,有了一点面向对象编程的感觉。

InputHandler负责处理输入,Dispatcher和请求队列是一体的,Elevator负责从Dispatcher中索取请求,然后将乘客送往指定楼层。此次作业我没有将InputHandlerElevator直接联系起来,而是在他们之间加了一层调度器,做到了输入和电梯的解耦,为第三次作业扩展电梯做了准备。

  此外,调度器采用单例模式,与一些同学将调度器设为线程相比,单例模式的方法比较简便,减少了一些线程不安全的可能性。

  电梯线程和输入线程同时工作,提高了处理请求的效率。

缺点:采用了轮询,没有使用wait()notifyall(),导致CPU时间较长,强测某些数据CPU时间长达15.79s,资源浪费较大。

  采用FAFS的调度策略,电梯利用率不高。

  采用的是托盘式的生产者消费者模式,没有使用某些同学所谓的look策略,第二次和 第三次作业又都是在第一次作业的基础上直接扩展,因此降低了性能。

  没有弄清sychronize的机制,直接锁方法,降低了性能。

内聚性和复杂度分析:由上图可知,第一次作业各方法的结构化程度和循环复杂度较低,方法间的耦合度较低,类的循环复杂度也较低。并且各方法的规模,控制分支数目以及类总代码规模也都不大。因此,设计架构还是比较清晰的。

 

UML图:

OO电梯单元总结 随笔 第4张

设计原则检查:

Singe Responsibility Principle:除了Elevator中的run方法稍稍复杂了一点之外,每个方法都只负责执行单一功能,符合单一职责原则。

Open Close Principle:由于采用轮询,显然不符合开闭原则,并且电梯并非采用观察者模式注册进调度器,后续新增电梯显然需要修改调度器,因此不符合开闭原则。

Liscov Substitution Principle:由于不存在子类,因此满足里氏替换原则

Interface Segregation Principle:由于不存在接口,因此满足接口分离原则。

Dependency Inversion Principle:由于只有1中电梯,因此满足依赖倒置原则。

 

第二次作业

Task完成单部多线程可捎带调度(ALS)电梯的模拟。

设计策略:大体沿用第一次作业的架构。

  Dispatcher采用单例模式,但由于此次作业要求捎带,笔者理解的捎带是请求方向与主请求相同着方符合捎带条件,因此为了方便,Dispatcher中的请求队列扩展为上队列和下队列。当一个请求输入时,InputHandler调用DispatcherputRequest方法,将请求放入相应的请求队列。仍然采用消费者生产者模式,Dispatcher相当于一个托盘,InputHandler是生产者,往托盘里放东西,Elevator是消费者,从托盘里取东西。

  由于第一次作业CPU时长较长,此次作业为了缩短CPU时间,采用了wait()notifyall()的方式,电梯请求一个请求时,如果当前上队列和下队列中请求个数均为0,则wait()直到被一个新的输入唤醒。

  由于存在捎带的问题,因此电梯类中存在自己的请求队列,表示当前进入电梯内部但是尚未完成的请求。在捎带的处理上,我采用主请求驱动的模式,可分为两类捎带:确定一个是主请求后,电梯从当前楼层前往主请求起始楼层的过程中,如果某一楼层恰好有请求者,且该请求者的到达楼层在当前楼层与主请求的起始楼层之间,则视该请求者为捎带,进入电梯内部请求队列insideRequest0;主请求者进入电梯后,在前往其目的地的途中,如果某一楼层有请求的方向与电梯当前运动方向相同,则将该请求视为捎带,进入电梯内部请求队列insideRequest,并且根据当前insideRequest的情况更新电梯需要到达的最大或最小楼层。

  对于电梯如何停止的问题,笔者的电梯中run方法里while循环的条件为调度器的上下请求队列中还有请求或者StopElevator中的停止标志尚未变为true(此处过于累赘,将在缺点中分析)。在InputHandler中,笔者将所有正确的请求以及null都加入了Dispatcher中的请求队列,其中null加入上队列。当Elevator调用DispatchergetUpRequest时,如果发现主请求为null,则breakwhile循环,结束电梯线程。

  此处还有一点需要提示,由于将null加入了上队列,因此从上队列中取出的请求将变得不再“安全”,因为取出请求是为了获得该请求的index或起始楼层或到达楼层,而如果该请求为null,将引发错误。因此每次get到一个请求后,需要判断该请求是否为null,如果取出的请求是主请求,则符合break的条件,如果不是在取主请求时得到的null,则需要跳过该请求,寻找下一个请求。

  并且,由于笔者在调度器中分了上下队列,为了便于在取主请求时有个依据,因此笔者自己创建了一个类ResWithIndex用于存储请求,以上提到请求队列的ArrayList均是ResWithIndex类的。ResWithIndex的属性包含了indexpersonRequestInputHandler每调用一次Dispatcher中的putRequest方法时,ResWithIndex中的index++。因此当上下队列中均有人时,依据index的大小选出主请求。

类图:

OO电梯单元总结 随笔 第5张

 

方法和类的复杂度分析:

OO电梯单元总结 随笔 第6张

OO电梯单元总结 随笔 第7张

OO电梯单元总结 随笔 第8张

OO电梯单元总结 随笔 第9张

优点:个人感觉在设计架构上,此次作业写的还是比较清楚的,各个类各司其职,相互之间通过信息传递进行交流。Dispatche采用单例模式,简单清晰,省去了一些同学一开始将输入类设计为线程,存在bug无法解决最终重构的烦恼。此次作业对第一次代码的复用率很高,除了修改调度器中保存请求的请求队列,电梯执行指令的方式上加上捎带功能,以及改变电梯类的停止条件外,其余均没有重构。

缺点:此次作业的缺点十分明显。

  while的循环条件不对。细心的同学应该会记得,我在谈论设计架构的过程中,提到了我的电梯类while循环的条件是调度器的上下请求队列中还有请求或者StopElevator中的停止标志尚未变为true,而在run函数的具体实现中,又是采用主请求为nullbreakwhile循环的,显然,每次循环时while中的条件都是满足的,因为取出的主请求为null的那次循环中,判断while条件时null仍存在于请求队列中。因此,while的循环条件应改为true

  无脑使用synchronized锁住方法。仍然没有深入理解synchronized的机制,起初尝试只锁对象,但引发了死锁,后没有深入学习,为保线程安全采用synchronized锁住方法的方式,降低了程序的性能。

  不符合实际电梯的运行规则。笔者的捎带分为两种情况。第一种情况的捎带显然不符合生活实际,但为了维护主需求驱动,第一次捎带的条件仍然为捎带者的起始楼层和到达楼层在当前楼层和主请求起始楼层之间。该设计可能导致电梯的性能降低。

  空间开销没有最简。从类图中可以看出,Elevator中存在insideRequest0(第一次捎带已进入的请求的队列)和insideRequest(第二次捎带已进入电梯的请求的队列)。实际上insideRequest0insideRequest在时间上没有交集,在操作insideRequestinsideRequest0已经为空,所以可以将两者合并,减少空间开销。

  Elevator中没有将输出(如arrive-楼层,open-楼层等)作为一个方法,因此在第三次直接复用电梯,但保证TimebleOutput线程安全时加对象锁十分繁琐,需要改很多地方。

  Elevatorrun方法结构化程度和循环复杂度较高,类的循环复杂度和总代码规模均较大。

  没有使用look策略,调度器的功能只停留在临时保存请求,没有很好的体现“调度”一功能。

  电梯一层楼的运动时长,开关门时间固化在了程序中,不够灵活。

 

内聚性和复杂度分析:正如缺点中所言,Elevatorrun方法结构化程度和循环复杂度较高,类的循环复杂度和总代码规模均较大。其余方法和类的复杂程度,结构化程度,循环复杂度均保持在一个正常的水平。

 

UML图:

OO电梯单元总结 随笔 第10张

设计原则检查:

Singe Responsibility PrincipleElevator中的run方法比较复杂,且没有做到对输出电梯信息在方法层次上的封装,因此该方法不符合单一职责原则。

Open Close Principle:电梯并非采用观察者模式注册进调度器,调度其中保存请求的队列是固化的,后续新增电梯显然需要修改调度器,因此不符合开闭原则。

Liscov Substitution Principle:由于不存在子类,因此满足里氏替换原则

Interface Segregation Principle:由于不存在接口,因此满足接口分离原则。

Dependency Inversion Principle:由于只有1中电梯,因此满足依赖倒置原则。

 

第三次作业

Task:完成多部多线程智能调度电梯的模拟。

设计策略:

  由于三部电梯均有不可到达的楼层,因此对于某些请求,需要将其进行拆分。

  我采用在调度器中拆分请求的方式,对合并性质相同的楼层,对性质特殊的楼层用switch case的方法进行讨论。在将请求放入请求队列之前,就已分配好将用哪些电梯搭载该乘客。想复用第二次的电梯,由于有三部电梯,因此调度器中保存了6个队列,每个电梯各占一个上队列和下队列。此外,由于存在数据冒险的可能,在调度器中另设6个容器队列,用来暂时保存被拆分指令的第二部分指令,当有乘客出电梯时,检索容器队列中是否有其对应的第二部分请求。由于一级调度过于冗长,因此将switch case的各种情况放在了FloorCase类,DispatcherFloorCase相互配合完成对请求的拆分和分配。

  虽然此次作业规定电梯有不能到达的楼层,笔者在设计之初,便把各请求分好,因此即使不对电梯做楼层是否可到达的处理,由于没有对应的请求,电梯也不会在其不可到达楼层停下来,因此,笔者在电梯中没有楼层处理部分,除了寻找请求的队列不同之外,笔者的电梯与第二次没什么不同,但在寻找主请求方面做了一点改动,如果在寻找主请求时,该层楼层存在请求,则其作为主请求。

  由于有三部电梯,TimeableOutput不保证线程安全,因此笔者采用了一个对象锁,创建了一个单例模式LockOutput,用来锁住输出。

  在设计模式上,除了沿用前两次作业的生产者消费者模式,此次作业新增工厂模式。ElevatorAElevatorBElevatorC都是Elevator类的继承,Elevator是一个抽象类,没有任何属性和方法,同时创建了FactoryFactoryAFactoryBFactoryCFactory是个接口,实现了创建一个电梯的方法createElevator,三个子工厂分别是Factory的继承,通过改写createElevator生产出特定种类的电梯。

  在电梯的停止问题上,笔者采用了和前两次不一样的方式,使用waitnotifyall的方法,并且null指令不入队。由于寻找捎带请求时采用的是for循环检索方式,因此不存在请求队列中没有指令但是想get一个指令而导致出错的情况,仅在得到主请求时,可能发生请求队列为空但是电梯要求请求的情况,因此仅在调度器的getAllRequest的方法中使用wait,并结合停止标志和相应容器队列中请求个数判断wait,在输入线程调用putRequest和把请求从容器移至请求队列时notifyAll。电梯中while循环的条件为StopElevator.getInstance().returnStopFlag() == false

|| Dispatcher.getInstance().getRequestListANum() != 0

                    || Dispatcher.getInstance().getContainANum() != 0

  并且,如果取出的主请求为nullnull并不在主请求中,但是由于主请求被初始化为null,因此队列为空时得到的主请求为null)时,break,两个条件相结合判断电梯的运行情况。

类图:

OO电梯单元总结 随笔 第11张

 

方法和类的复杂度分析:

OO电梯单元总结 随笔 第12张

OO电梯单元总结 随笔 第13张

OO电梯单元总结 随笔 第14张

OO电梯单元总结 随笔 第15张

OO电梯单元总结 随笔 第16张

OO电梯单元总结 随笔 第17张

OO电梯单元总结 随笔 第18张

优点:

  各类仅完成自己地工作,各司其职,最大程度地降低了耦合。

  除了对调度器做了一些改动之外,几乎没有重构,因此当许多同学还在为电梯发愁时,笔者早已完成了电梯。并且,三次电梯作业重构部分都不大,可见一开始就选中一个易于扩展的架构是多么重要(当然,这点基于不怎么考虑性能的情况,对于追求性能的大神,本菜鸡认为look算法才能最大的实现性能和架构的双赢,并且最大的体现调度器的“调度”这一功能)。

  采用了工厂模式,在一定程度上开闭原则。

缺点:

  在某些类的实现上过于复杂,不够简洁。笔者的第一版代码只有以及调度器,长达500+行,超出了checkstyle对于类的行数的限制,因此笔者不得不将调度器中的某些方法移至FloorCase中,电梯也过于复杂。

  不符合开闭原则。在调度器的设计上,没有像大多数同学一样,先判断是否能直达,不能直达的情况再寻找能够运送的电梯,而是直接对楼层进行分类,逐类讨论,因此如果增加楼层或者改变到达楼层,拆分请求这一部分需要重构。没有采用观察者模式,将电梯注册进调度器,而是将三部电梯的请求队列固化在了调度器中,因此如果电梯便须,调度器需要重构。

  由于请求在输入的时候便被拆分并且放到了相应的电梯请求队列或容器队列中,因此不能做到根据当前电梯的情况最科学的投放请求,调度器的功能没有被最大程度地体现。

  由于几乎直接沿用第二次的电梯,因此第二次电梯中存在的问题,第三次电梯也有。

 

内聚性和复杂度分析:

  由上图可知,部分方法的结构化程度和循环复杂度较高,方法间的耦合度较高,类的循环复杂度较高。部分类总代码规模比较大。

 

类图:

OO电梯单元总结 随笔 第19张

 

设计原则检查:

Singe Responsibility Principle:由图可知,部分方法比较复杂。但笔者认为,某些方法本身就比较复杂,在笔者能力范围内没法儿拆分,因此虽然可能不符合单一职责原则,但是在方法地职能上,划分还是比较清楚的。

Open Close Principle:在调度器的设计上,不符合开闭原则。但创建电梯时采用了工厂模式,因此此部分符合开闭原则。

Liscov Substitution Principle:三类电梯是电梯的子类,任何电梯可出现的地方,均可用三类电梯中的任意一类代替,因此满足里氏替换原则。

Interface Segregation Principle:因为客户只有一种请求,因此满足接口分离原则。

Dependency Inversion Principle:由于与客户交互时,只依赖于抽象接口,因此满足依赖倒置原则。

 

分析自己程序的bug

  十分幸运,三次作业均未在公测、强测和互测中被发现bug

  但是,自己编程时,对于电梯如何停止这一问题,比较头疼,最后总结出采用往调度器的请求队列中putRequestnotifyAll,在电梯getRequest时结合相关条件wait,电梯的while循环条件根据程序实现具体的方式选择循环的条件,就可以正常停止。当然,由于不存在总队列,并且一直无脑synchronized方法,因此也没怎么出现过线程不安全的问题。

  三次作业中,在第二次和第三次自己测试的时候发现了程序中存在的bug。第二次时,由于将null加入了上队列,一开始并没有判断取出的请求是否为null,如果为null,则continue,导致测试时发现程序抛出了异常,加以改正后解决了问题。第三次作业中,一开始wait外层的if条件没写好,只考虑了StopElevator标志是否已经变为true,没有考虑到还需要考虑容器队列中是否仍请求,因此发生了某些乘客无法到达指定楼层,电梯线程便已经结束的问题,后来加以修改解决了该问题。

  由于多线程无法调试,因此我只能采用打印语句进行调试。特别是低三次作业中,发现自己的程序过早结束后,在三部电梯线程结束时,答应出了各电梯电梯内部请求队列,电梯容器队列中请求的个数,从而较快地定位出问题。

 

分析自己发现别人程序bug所采用的策略

  在第一次电梯作业中,看了几位同学的代码,感觉同学们的设计架构都差不多,没有发现bug,因此交了两个空数据以求凑齐活跃度,不料不小心hack中了一位同学(可能是由于时间原因正好没看到该同学的代码)。

  第二次电梯作业,没有结合相关程序测试他人代码的bug,运气较好,输入的一组测试数据正好发现了某位同学的bug

  第三次作业,使用了评测机对同学的代码进行评测,但评测时间不长,因此没有发现bug

  电梯单元的互测与第一单元相比,在第三次作业中使用了评测机,其余均是通过看代码或输入测试样例发现bug,但没有像第一单元一样,系统地用自己写的测试样例对他人代码进行测试。

 

心得体会

  笔者感觉,电梯作业与第一单元的求导系列相比,更能体现面向对象的思想。类与类划分得比较明确,每个类各司其职。且第一单元只涉及一个线程,因此不存在线程不安全的问题,而第二单元线程安全是一个重要的训练点。在电梯系列作业中,主要通过加锁方式解决线程不安全的问题,但惭愧的是,笔者的三次作业都是无脑锁方法,因此感觉在使用synchronized方面,还有很多需要学习。

  在设计原则上,第二单元的设计原则明显比第一单元讲究。SRP原则在第二单元体现的最为比较明显。关于开闭原则,笔者的程序实际上没有很好的遵循,因为程序中还是存在固化的设计。

  在设计架构方面,笔者感觉第二单元的架构会比第一单元清晰。第一单元每次作业笔者都进行了重构,而且每次都感觉是在面向过程编程,而第二单元后两次作业沿用的都是第一次的架构,并且第二单元完成三次作业的时间相比第一单元少很多,每次作业平均花费时间只有第一单元每次作业的一半左右。在正确性上,笔者实际上没有对自己第二单元的作业进行较多测试,因此,再次感觉一个良好的架构对编程的效率和正确性都提供了很好的保障。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄