OO第二单元作业总结
第一次作业
- 类图

- 顺序图

-
同步块的设计和锁的选择:
- 把Requestlist写成了单例模式的线程安全类。
- 以类名作为锁,方法上加同步。
- 方法内对于共享的单例读写,必须同步
-
调度器设计与交互
- 第一次作业只有一部电梯,体现不出调度器的作用,加入反而显得冗余,所以没有实现。
- 线程交互通过读写Requestlist单例进行。
-
可拓展性
- 没实现调度器显然是难以拓展的。
- 电梯类运行逻辑实现较好,使得未来的电梯运行部分都不必要再改,只需要修改操作的缓存区。
- 电梯采用LOOK的运行策略,总是优先服务方向上最远的请求,再改变方向。
- 分别写了三个具体的运行方法,使得morning,night,random下电梯的表现有所不同。这里实际上可以采取策略模式优化掉switch。
-
程序BUG分析
- 注意到对于缓冲区的状态判断也称为了封装在Requestlist内的同步方法。而在实际调用中,一个while的条件可能连续调用几个这样的判断方法,所以得到的三个布尔值可能不是同步的。
if (RequestList.isEnd() && RequestList.isEmpty())- 修复这个问题,既可以进一步封装好单例线程安全类,使多个条件判断同步,也可以取消单例。
- 在后续作业中直接取消了单例(其实现在想想进一步封装迭代效果可能更好)
第二次作业
- 类图

- 顺序图

-
同步块的设计和锁的选择:
- 公共缓冲区Stashqueue
- 每个电梯自己还有一个缓冲区Requestlist
- 以上述两个类的对象为锁
- inputthread向Stashqueue写入,dispatch读Stashqueue,向某个电梯的Requestlist写入,以及电梯读写Requestlist,均需要同步。
- 取消了单例,而全部在调用类的方法前用相应的类的实例加锁。这样即使一次调用多个类的方法作为条件判断的值,也不用担心同步问题。
-
调度器设计与交互
- 调度器把公共缓冲区Stashqueue的内容一次性全部分派到每个电梯自己的一个缓冲区Requestlist,然后清空公共缓存区。
- 调度器采取自由竞争的方式。
- 备选版本:设计了如下规则,但是考虑到获取单电梯的多个状态是异步的,比较多个电梯同一个状态还是异步的,一开始担心这种异步影响过大,就直接注释掉了而采取自由竞争,最后和同学对比发现即使有异步但是效果还是更好的。
1.向相同电梯运行方向上加 2. 在1的情况下向最靠近电梯的方向上加 3. 12都不符合,往向折返需要时间最小的加 4. 经过123后,有多个符合,考虑任务均摊 5. 4后还有多个满足,随机派发 -
可拓展性
- 第一次作业后电梯的移动策略已经可以不必再修改。
- 而电梯的调度架构这次也成型。
- 未来只需要修改dispatch类的调度方法和elevator类的属性即可完成迭代。
-
程序BUG分析
-
强测出现了死锁,电梯类中的run方法没有考虑到电梯内还有乘客的情况
-
synchronized (requestList) { if (requestList.isEnd() && isEmpty() && requestList.isEmpty()) { return;//在电梯里没有乘客,缓存区输入结束且为空的时候结束 } while (requestList.isEmpty()) { //这里只考虑缓存区为空就等待,这样在电梯里还有乘客但是缓存区空的时候造成等待,使接下来运送乘客的代码无法执行,输入已经结束就不会再有notify,造成死锁 try { requestList.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } }
-
第三次作业
- 类图

- 顺序图

-
同步块的设计和锁的选择:
- 沿用第二次作业的设计
- 但是为了实现换乘,把PersonRequest做了进一步的封装。现在用保存有PersoRequest的ArrayList的Route类来替代原来单一的PersonRequest。实现同一个Person的Request不分离。
- 为了能够识别结束,对于任务数量单独引入一个线程安全的类NumLock来控制。在input时增加,在分解的任务全部完成后减少,为0终止。
-
调度器设计与交互
- 架构与第二次作业相同。
- 调度器向电梯分派任务采取贪心的方式。
- 调度器会根据请求计算最佳时间和并根据电梯运行状态分配。
- 实际上是计算后把原来的PersonRequest分解,成一组的PersonRequest存入Route,然后把Route打回缓存区。
-
可拓展性
- 功能设计上,引入新的类Route来保存一个人的换乘路线,并把缓存区所有元素类型都换为Route,以支持乘客请求的分解和再合并,未来能够对一个人的行程做更细化的处理。总体上的架构不发生改变,还在原来基础上提高了对请求的处理能力,能够保证完成电梯运行和任务调度的功能。
- 电梯实际只修改运行速度和容量,而只接受符合楼层类型的任务,不需要修改运行逻辑,任务分配由任务分解和调度方法来完成。未来还能支持引入更多类型的电梯。
- 性能设计上,采用贪心只是寻找局部的最优解。如果需要更换策略,只需要改写调度方法部分;如果需要修改电梯运行方法与之配合,也只需要修改电梯的运行逻辑部分,而其他部分不需要改动。能够在功能设计的支持下继续深入探究更佳的性能。
- 此外贪心计算运行时间的同时其实可以同步生成一套分解结果,而不必要计算后在去分解,稍有些面向过程的味道。
- 个人认为在保障功能设计的基础和可拓展性之上,已经为性能设计的可拓展性提供了最有利的条件。
-
程序BUG分析
- 具体实现的时候,在计算时间的贪心里用了递归。但是在Route里对应的分解任务算法没有使用递归,导致不符合类型的任务被分配到电梯里,就出现了WA。
发现BUG的方法
- 个人倾向找TLE的BUG,常用一些超大规模的数据进行轰炸,看是不是会有死锁问题。测试工具主要是打包成jar,提供一个带时间戳的输入,一个根据时间戳输入定时投喂的C程序,然后.bat执行重定向输入输出。
- (其实方法主要都用在找自己的BUG上了)
- 自己测试的时候经常有可复现的死锁,在前两次作业根据测试结果也对锁做出了很多修改。
- 与一单元作业区别:
- 多线程的测试最大的问题在于每次执行的顺序都不相同,即使是确认有bug也不一定百分百复现,可能需要多次执行才能碰到恰当的次序引发错误。
- 输入输出的数据都变得更为复杂,需要截取/投放时间戳。
心得体会
-
线程安全:
- 单个线程操作自己栈内的变量是没有线程安全问题,线程安全本源在于线程交互,为此我们设计了锁,信号量等实现线程之间交互的安全。
- 类比进程的互斥和同步,线程安全也可以具体分为线程的互斥和同步来实现。
- 最好的测试方法是读静态代码。所有的while轮询,同步块语句处都需要仔细阅读。
-
层次化设计:
- 层次化设计首先要把各部分分开。具体来讲就是从读完题以后,选择你想要建立几个类,把每个类当成层次化设计中的一个模块。这次作业中有电梯线程,输入线程,调度器,缓存区等。
- 在各个模块里迭代开发,尽量利用之前的代码,就会使得下一次作业写的更舒适。这就需要“瞻前顾后”,如果一时难以顾及可拓展性,可以适当参考一些设计模式的思路,如读写锁,状态/策略模式,运用到自己的架构里。

浙公网安备 33010602011771号