面向对象第二单元总结
OO 第二单元 电梯作业总结
1、总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步快中处理语句直接的关系
三次作业都是采用锁住共享对象(物理锁)进行同步控制,并在迭代中逐步深化理解了多线程的本质。
-
第一次作业
本次作业只有一个共享对象,即存放整个电梯所有乘客的类AllQueue。共享它的也只有两个线程,即InputThread和Lift。每次在对AllQueue进行更改时,只需要synchronized(all)即可。
另外,由于我的AllQueue本质上存了20个WaitingQueue,因此我并不知道锁住AllQueue是否代表锁住里面的所有元素。因此当我对层操作时,往往是只锁住那个层,而不所整体。
//InputThread if (request == null) { synchronized (all) { all.close(); all.notifyAll(); break; } } else { // a new valid request WaitingQueue queue = all.get(request.getFromFloor()); synchronized (all) { queue.set(request); //把请求加入到相应的队列里 all.notifyAll(); } }
//Lift while (true) { if (all.isEnd() & inLiftPassenger.isEmpty() & all.allIsEmpty()) { if (isOpen) { close(); } return; } synchronized (all) { if (inLiftPassenger.isEmpty() & all.allIsEmpty()) { try { all.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } dispatcher.begin();
-
第二次作业
本次作业的共享对象增加了一个WaitingPool,主要原因是改变了输入对象,在InputThread和Lift之间加了一个过滤器Scheduler,将乘客从大的等待池中挑选出来放入一个个的等候队列中。
因此,InputThread与Scheduler会同时对WaitingPool操作,Scheduler和Lift会同时对AllQueue操作。
//InputThread if (request instanceof PersonRequest) { // a PersonRequest synchronized (pool) { pool.addRequest((PersonRequest) request); pool.notifyAll(); }
//Scheduler synchronized (pool) { if (pool.isEnd() && pool.noWaiting()) { for (int i = 0; i < allQueues.size(); i++) { synchronized (allQueues.get(i)) { allQueues.get(i).close(); allQueues.get(i).notifyAll(); } } return; } if (pool.noWaiting()) { //请求队列里没有新来的人了,那就等着// try { pool.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } else { //kernel code }
-
第三次作业
第三次作业在锁与同步块的设计上与第二次作业没有任何区别。
2、总结分析三次作业中的调度器设计,并分析调度器如何与程序中的线程进行交互
为了更好地总结我的调度器和其它类的关系,我做了几个ULM图
- 第一次作业
- InputThread -> 输入类,为单独一个线程。
- WaitingQueue -> 每一层的等候队列,本质是装着Request的容器。
- AllQueue -> 整个电梯的等候队列,本质装了20个WaitingQueue。共享对象。
- Lift -> 电梯类,单独线程。只负责电梯的运动控制。
- Dispatcher -> 调度类,是Lift电梯类中的一个属性。Lift类开始run时初始化Dispatcher,并把电梯本身传入。调度类根据电梯内的人、电梯等候队列的人、电梯的运行状态和当前pattern来判断下一步做什么运动。
Dispatcher 有一个begin函数,lift线程开启后每次循环都会调用其一次。其中会针对不同pattern选择调用不同函数,如Morning/Night/Random。
每一个函数内会根据当前模式的特点来控制电梯下一步运行策略。我主要采用的是Look算法,即如果电梯没满,就去找同方向的最远等候乘客。每移动一层都要判断一次,可以实时改变最优策略。
-
第二次作业
第二次作业在第一次的基础上增加了电梯数量,在3-5之间不等,但电梯的种类和第一次一样。因此我的的整体架构相对于第一次并没有太大改变。添加了WaitingPool类,用来存储输入的乘客;添加了Scheduler类,用来把等候池里的人一一分配给不同乘客,此类单开一个线程。除此之外,几乎没有太大的改变。
整体思路:通过InputThread类来输入请求,把其中的PersonRequest全部放入WaitingPool中管理的pool里(本质是个ArrayList);把其中的ElevatorRequest放入一个大家都能访问的电梯容器里。Scheduler获取pool里的乘客,根据模式不同、每个电梯状态的不同来把每个乘客放进不同的电梯等候队列里(AllQueue)。只要分配好每个电梯的等候队列,后面的操作就完全不用再管了,优化也仅限于分配策略那几十行,特别清晰。
这次的调度器还是Dispatcher,和第一次作业完全没有改动。
-
第三次作业
第三次作业的调度类和第二次作业相比没有改动,只是针对不同的电梯类型对Scheduler分配器进行了些许改动。
3、从功能设计与性能设计的平衡方面,分析和总结自己第三次作业架构设计的可扩展性
- 画UML类图
- 画UML协作图(sequence diagram)来展示线程之间的协作关系(别忘记主线程)
-
扩展性分析
可以看到,由于第一次作业的架构比较合理,本单元二三次作业的迭代非常顺利。
后续扩展,如果增删电梯的运动模式。比如除了向上走、向下走,还能有加速上行、加速下行等,类似对电梯本身的改动。只需要在Lift类里增删函数即可。
如果增加电梯的种类,比如除了第三次作业的ABC型电梯,还有其他模式,那只需要更改分配器,针对不同的要求把不同的乘客分配出去。完全不需要修改已经写好的Lift类(因为我一直以来的策略就是,无论针对哪种电梯,电梯本身的属性不改变,只是通过外界对它输入乘客的操控来区分类别)。
唯一的问题就是,如果增删运行模式(即Random/Night/Morning),在我的设计模式下可能需要更改的地方比较多。需要对每种电梯的该模式均做修改。究其原因是我没能将所有电梯的调度策略归一,而是零散、难以组织起来的。
4、分析自己程序的bug
分析未通过的公测用例和被互测发现的bug:特征、问题所在的类和方法
特别注意分析那些与线程安全相关的问题(特别要注意死锁的分析)
-
第一次作业
未通过公测的bug在于优化失误!最开始并没有使用Look算法,而在最后一刻将算法改成了Look。但是忘记了我曾经在等候队列里取乘客时是只取第一个... 因此调度器要取的和实际取出来的人不一样,导致出大锅。
这个bug的产生主要来源于优化后做的测试太少了,如果多几组数据是肯定能发现的。
另外,产生这个bug也是因为我函数命名的模糊。waitingQueue中我获取第一个乘客的函数是getPeek,而到了AllQueue里函数就变成了getPassenger。导致到了后期我没有判别出来它是在取第一个元素。
-
第二次/第三次作业
在强测与互测中都没有被发现bug : >
5、分析自己发现别人程序bug所采用的策略
列出自己所采取的测试策略及有效性
分析自己采用了什么策略来发现线程安全相关的问题
分析本单元的测试策略与第一单元测试策略的差异之处
-
自己所采取的测试策略
我的测试策略主要是手动构造一些针对我自己的优化与可能出现的漏洞设计的数据。往往是先随机放机组数据做正确性测试,再在每次优化与修改中放针对性的数据。
在第三次作业中,被中测卡了一个不公开的点导致一直过不去,于是借助了zsm同学的评测机,最终发现是一个判断语句边界条件设置错误导致的C电梯在不可停区域停留。
本单元作业的输入与输出中,在dhy同学的推荐下,我学会了使用管道流的方法,使得能从文件中输入,在文件中输出。这种自测技巧极大地提高了我的测试效率~
对于有效性,在第一次作业中,我遇到过本地测试与评测机结果不符的情况。究其原因应该是时间戳的不一致性。即同样的测试数据,本地的运行结果和评测机上的运行结果大相径庭。我最终也没有找到非常有效的解决方法。
-
发现线程安全相关的问题的策略
-
形式化验证
之前在自测时发现程序出现了:debug模式下是对的,正常跑起来就是错的;printf后就是对的,正常跑起来就是错的 的奇怪现象。后来一行行代码看完后发现是换乘时忘记特判。例如如果一个来自2层的人要去3层,恰巧乘坐B电梯比较合适,会从A电梯换乘,此时会导致在2层出、进、出、进无限循环...
但说实话,我在线程安全问题上出的错误都非常非常明显。随便一个测试数据就能发现,要么程序没停下来,要么一个人进了两个电梯。产生bug的无非就那几个对共享数据操作的代码中,找到还是很容易的。
-
-
测试策略与第一单元的差异
第一单元的测试主要是静态的,并且结果即使形式不唯一,但最终答案唯一。可以通过化简与转化把所有人的正确答案化成一个形式。评测机也很好写,测试也很好测试。而本单元正确性检验就是个很复杂的问题,且就算发现了bug也很可能复现不出来,比较迷幻。
6、心得体会
从线程安全和层次化设计两个方面来梳理自己在本单元三次作业中获得的心得体会
层次化设计
本单元作业让我深刻理解了架构的重要性和层次化设计的好处。我每一次的迭代都是在之前的代码上增加类,层数越来越多,而不改变底层实现。例如增加了电梯个数,就只需要增加一层Scheduler将乘客筛分’增加了电梯种类,甚至一层也不用加。
在作业的迭代中,我纠结过两个大岔路:
- 1->2 :是否要单开一个Scheduler类来分配电梯里的人?还是用自由竞争策略?
- 2->3 :是否要针对多种电梯设置换乘?
很显然,如果不单开分配器,代码会简单很多;不换乘,2-3的迭代量几乎为0。
但是无论是从层次化角度分析,还是SOLOD 中的OCP原则来看,设置分配器是一件非常符合结构与逻辑的操作。如果新加调度器,便永远不会重写已有的设计,无需修改现有的实现,只通过扩展来增加新功能。但是自由竞争无论怎样都需要改动电梯类。
至于是否设置换乘,写换乘算法后性能是否真的可以提升,我想:
也许第一次作业单线程可以实现
也许第二次自由竞争的性能 > 调度分配
也许第三次不换乘性能 > 苦心经营的换乘
也许一个优化把你送入C room…
但在不断调试、优化与钻研中,我们代码能力/面向对象思想的提高是无法用分数衡量的
我们在优化中精进电梯
更是在优化中精进自己
致谢
多线程单元(尤其是第一次作业)的作业难度的确不小,对于初次接触的我是一个比较艰难的挑战。但是三周里有许多许多同学以不同的方式在帮助我~在此对他们表示非常诚挚的谢意
感谢dhy同学提供的自测方法/zsm同学的美丽评测机/wjy同学的设计分享/xyf同学的架构建议和精神支持~
各位都是人间美好!