OO第一单元(求导)单元总结
这是我们OO课程的第二个单元,这个单元的主要目的是让我们熟悉理解和掌握多线程的思想和方法。这个单元以电梯为主题,从一开始的最简单的单部傻瓜调度(FAFS)电梯到最后的多部多线程智能调度(SS)电梯,我们需要掌握的多线程知识也从简单的线程间交互与同步到多种多线程模式相结合的复杂线程调度系统。
一、作业分析
第一次作业
第一次作业要求实现的是单部多线程傻瓜调度(FAFS)电梯的模拟。即一次接送一个人的VIP式电梯。
这个版本的作业其实不是我提交时所用的版本,而是我在提交之后第二次作业之前进行修改优化之后的版本。我第一次作业提交时的那个版本可谓非常粗糙,没有给输入单开线程,线程与线程之间的交互采用的也是轮询即sleep后再询问。总的来说我最开始的版本就是通过main来接受输入,再把输入存到一个用来保存请求的arraylist中,之后电梯从这个arraylist中读取请求,再自行进行处理。在第一次作业之后,我对第二次作业的内容进行了猜测与思考,决定在原有的基础上进行一些改进以方便第二次作业。我首先进行的改进是将线程交互的方式改为wait和notify,也就是实现一个输入与电梯之间的简单的生产者消费者模式,存放请求的arraylist就是托盘。对此我发现有必要将输入单开一个线程以充当生产者,于是就有了之后的改进。当然改进之后的作业也存在许多拓展上的问题,最为明显的就是我的电梯运行方法的编写是完全的面向过程的,没有丝毫考虑接下来扩展的事,是注定要在下次作业中重写的。其次就是没有编写调度器,指令时直接由输入传递给电梯的。
这次作业改进前的结构是只有main和电梯,改进后为简单的生产者-消费者模式。
这次作业的优点就是不容易出错,即使出错了也很容易追查出问题出在哪里。缺点也很明显,就是无论架构还是电梯运行算法的编写都极其不具有延展性,且后遗症会很多,虽然在之后进行了一定程度上的改进,但依然会给第二次作业造成不小的麻烦
程序出现的bug:由于此次作业比较简单,所以未出现bug
查找别人bug所用的策略:用一些比较边界的条件进行测试
第二次作业
第二次作业要求完成单部多线程可捎带调度(ALS)电梯的模拟
这次作业是我时间化的最多的一次作业,也是我完成最多工作的一次作业,完成了本系列作业的基础架构,为之后的第三次作业省下了不少力气。
这次作业相对上次作业我进行了极大的改进。通过与同学的交流与自己的思考,我初步定下了自己的架构。输入线程将输入传到总的请求队列里,调度器接受总请求并通过将其加入电梯的任务请求队列来将其调度给电梯以分配任务,电梯负责实现作业要求的算法。虽然这次作业中调度器只起到了从总请求队列将请求原封不动地搬到电梯的任务队列中,但考虑到第三次作业一定会有多部电梯所以我觉得还是十分有必要设计一个调度器。调度器的实现我采用观察者模式,把电梯定阅进调度器中,通过update函数将新的请求加入到电梯的任务队列之中。电梯实现的捎带算法的方法是,遍历任务队列,与电梯当前运行方向同向的上电梯请求加入到电梯的载客队列中,并将其从任务队列中删去。电梯每到一层都会检查载客队列与任务队列以上下乘客,电梯的目的地是所有载客请求中最远的,每次电梯搭载新的请求都会更新目的地楼层。当电梯达到目的地楼层后一次循环就算完成。线程的结束是电梯的任务队列中只剩下NULL请求且载客队列中没有请求。
这次作业的优点就是延展性很好,已经做好了下次拓展到多部电梯的准备。在电梯算法实现时我曾思考过是将指令在调度器中处理完毕后再传给电梯让它依次执行还是由电梯来实现捎带,调度器只负责分配任务,后来我考虑了多部电梯的情况,觉得如果让调度器来实现捎带会使得调度器类十分繁杂,因此我选择有电梯来处理算法实现捎带。在设计这个架构时,我极力减少了线程与线程之间的交互,以避免过于复杂的同步锁的情况出现,因为这样容易出现死锁,而死锁在debug时是十分难以发现和修复的。架构中电梯与调度器的交互只有订阅与更新。另一方面,之前留下的电梯的算法实现的问题也得到了改进,虽然没有实装一些电梯参数的初始化,但也设了相应的变量,为初始化参数做了准备。
当然缺点也是有的,由于我将电梯的整个捎带算法实现在一个方法中,因此不符合checkstyle(类行数过多),为解决问题我胡乱地将部分代码拆分出来形成新的方法,使代码可读性急剧下降。
作业出现的bug:一开始没有加载客队列因此出现了电梯造人的情况,即先下电梯再上电梯。
查别人bug采用的策略:自动生成数据进行debug
第三次作业
第三次作业要求实现的是多部多线程智能(SS)调度电梯的模拟
由于上次作业我做了许多工作因此这次作业对于我来说相对比较简单,我只需要实现调度和电梯的参数初始化。由于电梯多了规定的可达楼层,因此我选择在电梯类中创建一个int数组用来表示楼层是否可以停靠,即a[n] = 1就表示n楼可以停靠,0表示不可停靠。另外还在电梯类中实现了一个方法canReach用于检测传入楼层是否可到达。其余电梯初始化参数相对简单因此跳过不提。
调度算法主要实现了两部分功能,第一部分是指令的分配,将请求分配给任务最少的电梯,第二部分功能是请求的分解,即将一个电梯无法完成的请求分配给两部电梯(没有需要三部电梯才能完成的请求)。拆分出的指令分为前置指令和后置指令,后置指令必须在前置指令完成后才能执行,因此需要交互。但由于这两个指令时分配在两台不同的电梯之中的,本着减少线程间交互的原则不能通过简单两个电梯线程之间的交互实现指令的前后执行。因此我继承了给定的输入接口文件中的PersonRequest类来实现了一个新的变量flag和本类类型对象nextRequest用以交互前后指令。当指令为后置指令时flag置为0,其余指令flag置为1。前置指令的nextRequest为后置指令,其余指令的nextRequest为NULL。每当请求执行完成后都会执setNextFlag方法将后置指令的flag置为1,电梯执行算法时会在循环中忽略flag为0的请求。这样就在避免线程间交互的同时实现了前后指令的顺序执行。另一方面的分配则相对比较简单,就是统级电梯类中两个队列的size大小,将新指令分配给值较小的电梯。对于两类指令的区分,调度器会先检查电梯是否可以同时到达出发楼层和目的楼层,如果三台电梯中均没有能同时到达的电梯,则找到能到达出发楼层的电梯和能到达目的楼层的电梯中能共同到达的楼层已完成请求的分解。
电梯的算法依旧沿用上次作业的ALS算法,只不过加入了检查电梯人数是否超载的环节。
这次作业的优点就是我做的非常快(只花了一个下午),正确率也很好,我第一次体会到了做好架构能带来的好处。还有的优点就是由于线程之间的交互非常的少,因此同步非常的简单,不容易出现死锁。缺点也有,就是性能问题,我实现的ALS算法在急转向时会出现开两次门的情况从而导致电梯的效率比较低。
作业出现的bug:没出现bug
查别人bug采用的策略:自动生成数据进行debug
二、总结与感悟
这次我最大的感悟就是做一个延展性好的架构的重要性的好处。之前一直都是重构一时爽,次次重构次次爽,这次是真的真香了。由于我在第二次作业中完成了整个架构,因此我第三次作业的代码复用率极高,几乎是百分之百,且完成的也非常快。另一个令我感悟很深的事就是这次学到的这么多模式,无论是单例模式、生产者消费者模式、工厂模式还是观察者模式,单独列出来都不足以解决第三次作业的电梯问题,但是将他们结合起来就可以很好的解决问题,模式之间的组合是解决复杂多线程问题的一条捷径。