BUAA OO Unit 2
HW2-1
题目要求与分析
本次作业的基本目标是模拟单部多线程电梯的运行。
第一次作业只要是起到多线程入门的作用,让我们初步了解多线程的实现方法以及注意事项。
数据结构
为了让作业有更好的扩展性(其实并没有),我将电梯与调度器分离开,调度器采用工厂模式,分为 Morning
, Random
和 Night
三种策略去实现。
用 Request
存储所有输入的请求,并且将请求按照楼层存放,便于查找。
在每个电梯中,用 passenger
存储电梯内的人员。用 Scheduler
接口做为电梯的“大脑”,负责向电梯传递指令,如 goUp
, goDown
, wait
, arrive
等。
调度算法
Random
模式: LOOK 算法Morning
模式: 等满 6 个人就接受乘客并上升,无人时自动前往 1 层Night
模式: 由于同时可以获得所有的输入,因此选择每次到达有请求的最高层,再向下。
线程
本次作业将输入和电梯都作为单独的线程来运行,一个负责向请求队列中添加请求,一个负责从请求队列中获得请求。
UML
UML 类图
UML 顺序图(sequence diagram)
bug
本次作业在强测和互测中均未发现 bug。
性能
由于只有一部电梯,变化相对有限,因此在强测中也获得了尚可的性能分数。
测试
由于时间精力有限,再加上多线程测试的复杂性,因此未能构造出有效的测试方法进行测试,因此也未发现屋内成员的 bug。
HW2-2
题目要求与分析
本次作业要求模拟多部同型号电梯的运行,并要求能够响应输入数据的请求,动态增加电梯。
在第二次作业中,添加了多部电梯,为了能直接在第一次作业的基础上进行扩展,我采用了二级存储队列的结构。
有了第一次作业的铺垫,第二次作业实现正确性实际上是较为容易的,其困难之处主要在于调度与分配。
数据结构
用 RequestPool
做为总请求池,只接受来自 GetInput
的添加和 Dispatcher
的访问和删除操作,每个电梯对应一个 PendingRequest
请求队列,由唯一的分配器 Dispatcher
将请求分配给各电梯对应的请求队列。RequestPool
和 Elevator
的关系同第一次一样,由各个电梯的 Scheduler
调度。
除此之外,由于课上提到了单例模式,因此在作业中也尝试将其实现,体现在 MainClass
和 RequestPool
类中。
调度算法
异zi想tou天luo开wang
第二次作业加入了多部电梯,难点在于调度。
由于电梯变换楼层会消耗大量的时间,因此我突发奇想的想要让每个电梯负责一部分楼层,这样可以最小化一般情况下电梯的移动距离。
但是,我没有考虑到开关门也需要一定的时间,多次换乘造成的开关门代价可能远远高于不换乘的移动造成的时间损失。而且,加入电梯的控制楼层以后,也大大提升了算法的复杂度:如上下乘客的计算,电梯的调度与分配器的分配等,使程序变得异常庞大。因此,其 debug 的复杂性也可想而知。
自wu信zhi的我并没有意识到这样做的复杂之处,直到我写完了代码...发现不但没有提升效率,在多数情况下,甚至比单部电梯更慢,有 RTLE 的风险,而且由于代码量大,复杂度高,也时常会出现难以追踪的 bug。于是,反复纠结过后,我决定放弃这复杂的代码,转而更为正常的调度方案。
最终的解决方案
由于上文所说的,我在一个并不实用的架构上花费了过多的时间,因此留给我的时间就变得极为有限了。在看过讨论区后,我采用了最短运行时间的调度方案,即在电梯中,定义 visualRunTime
方法,将请求虚拟的执行一遍,并返回所用时间,分配器会选择时间最短的电梯分配请求。
这样做的缺点在于 visualRunTime
的复杂度高,且会被反复调用,对于每一个请求,都会调用每一部电梯的 visualRunTime
方法,因此该方法极大程度的决定了整体的运行时间和效果,这也为后面的 bug 埋下了隐患。
线程
和上一次一样,电梯 Elevator
和输入 GetInput
分别对应两个线程。与上次不同的是,分配器 Dispatcher
也单独作为一个线程。
UML
UML 类图
UML 顺序图(sequence diagram)
结构示意图(手稿)
bug
由于代码结构的复杂性,本次作业的 debug 是十分痛苦的过程。
在中测和弱测中,出现了死锁的问题,原因是多层 synchronized
嵌套,以及当 wait 相等时间时候,更容易因为同时醒来抢占资源而发生死锁。
在强测中出现了两个 bug,即八、九两个点出现了 RTLE。
原因如下:
- 由于
visualRunTime
为考虑到电梯容量,因此模拟出的时间与真实运行时间有差距,导致一个电梯请求队列可能过载,因此出现分配不合理的问题。 - 由于
Dispatcher
会将RequestPool
锁定,且每获得一个新输入之后都会notify
Dispatcher
,因此当同时输入大量请求时,Dispatcher
只能单独处理,并且在处理时无法再获得更多的请求。解决办法是在Dispatcher
中适当释放锁,或在Night
等模式下接受完全部请求再notify
Dispatcher
。
性能
如前文所说,我的性能主要取决于 visualRunTime
方法,而由于该方法在设计上考虑不周全,因此在性能上的分数普遍不高。
测试
屋内有一人在输入全部为 ADD
电梯而没有乘客请求的时候会出错,奈何测试点非法,因此并没有测出屋内任何人的 bug。
HW2-3
题目要求与分析
本次作业要求模拟多部不同型号电梯的运行。型号不同,指的是开关门速度,移动速度,限载人数,以及最重要的——可停靠楼层的不同。具体的型号参数在下面给出。
吃一堑,长一智。吸取了第二次作业中架构过于复杂的教训,我在第三次作业中选择化繁为简。
数据结构
本次作业将所有请求按照楼层存储在 RequestPool
中。
调度算法
所有电梯整体上采用 LOOK
算法,在 helpful
的条件下自由竞争乘客。
注:
helpful
是RequestPool
中定义的一个方法
- 请求的开始楼层在电梯可到达楼层中
- 在电梯完成请求后(将乘客运送至距离目的地最近的可到达楼层),乘客距目的地的距离比之前更为靠近
当且仅当上述条件全部满足时,电梯认为是对请求有帮助的,即helpful
。
从上述关于 helpful
的算法中不难看出,虽然本次电梯加入了换乘策略,也进行了简单的判断,但在性能上却并不高。由于开关门均需要消耗时间,且换乘可能造成乘客实际所走过的路程大于原本需要走的路程的情况,因此这种换乘策略只能保证在某些情况下会获得较好的性能,而在一般情况下的效率则不一定会有所保证,这点从强测中也可以看出,对于不同的测试点,电梯的运行效率呈两极分化的趋势,即 99+ 和 80 均占有一定的数量。
线程
和第一次作业相同,电梯 Elevator
和输入 GetInput
分别对应两个线程。
UML
UML 类图
UML 顺序图(sequence diagram)
bug
由于架构较为简单,在强测和互测中均未出现任何 bug。
性能
在本次作业中,最优秀的性能也是最简单和最复杂的,要么毫无调度,从不换乘,要么考虑细致入微,实现最恰当的调度。而像我这种优化了一半的,在性能上最为吃亏。
测试
本次测出屋内两个 bug。(虽然由于多线程的迷惑性只有一个 hack 成功)
分别是:
- 在同时输入大量请求时,程序会不输出结果,发生
RTLE
。 - 在先加电梯,再输入乘客请求的情况中,会报错。
心得体会
在 debug 之前,我还不相信多线程的玄学之处,直到我真的遇到了加了 System.out.println
和不加能导致完全不同的输出结果的奇妙 bug 和 20 次运行总有一次会出锅的诡异情况 = =
这次作业,最开始是本着尽量不重构的思路去做的,但在实际写码的过程中,由于各种异想天开进行了代码的重构,且在性能上远不如最开始的性能。这反映出先动脑再动手的重要性。在决定从头开始之前,应当先确保这样做的好处,方法是在理论上对比新旧两种策略,也可以多和同学交流设计思路,避免浪费时间,写出并不能带来性能和安全性提升的代码。
总的来说,这次作业对我的多线程编程能力有了很大的提升,对于多线程,包括互斥,同步,死锁等也有了更深入的理解。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误