BUAA OO 2023 第二单元总结
电梯问题的难度我早已有所耳闻,还未开始就早已深深恐惧。
一、第一次作业
1.题目描述
系统基于一个类似北京航空航天大学新主楼的大楼,电梯可以在楼座内 1−11 层之间运行。 系统从标准输入中输入请求信息,程序进行接收和处理,模拟电梯运行,将必要的运行信息通过输出接口进行输出。
2.架构分析和实现
UML类图
协作图
架构模式
采用生产者—消费者模型
生产者:InputHandler 线程,主要负责接收 request 并将其放入托盘
消费者:6个Elevator线程,负责模拟电梯的运行,接送人员
托盘:ArrayList WaitList
InputHandler 接收 request 并将其放入 WaitList 中,然后由调度器Manager判断WaitList中的NowPerson应该进入几号电梯,再将NowPerson放入该电梯的OutList中,等待电梯的运送。
调度设计
1.电梯运输电梯等待队列里的人员的策略----采用LOOK策略
电梯中有两个WaitList,分别为OutList和InList,保存电梯外部没有进电梯的人,和电梯内部已经进电梯的人。采用LOOK策略的方法:
① 判断该楼层是否需要开关门,若需要则进行开关门操作
② 如果内部请求队列不为空,则继续向原方向前进一层
③ 若内部请求队列为空,则看外部请求队列是否有方向与电梯方向一致、并且位于电梯同层或电梯前方的人,若有,则电梯继续向原方向前进一层
④ 若没有,则电梯修改前进方向
2.调度器判断当前人员应该进入几号电梯的外部请求队列的策略
本策略是决定电梯性能的关键,一个好的策略可使得电梯有更好的性能。我经过思考后想出了如下策略:
设计一个函数,传入电梯的参数和当前人的参数可计算出该电梯的一个权值,分别计算出六部电梯的权值,将该人放入权值最小的一个电梯。
权值的计算方面,首先计算时间权重:
人和电梯同方向
{
人在电梯前面或同层:abs ( NowFloor - NowFromFloor )
人在电梯后面:
{
电梯向上:ans = max(向上的人的ToFloor, 向下的人的FromFloor)
电梯向下:ans = min(向下的人的ToFloor, 向上的人的FromFloor)
abs( ans - NowFloor ) + abs( ans - NowFromFloor )
}
}
人和电梯反方向
{
电梯向上:ans = max(向上的人的ToFloor, 向下的人的FromFloor)
电梯向下:ans = min(向下的人的ToFloor, 向上的人的FromFloor)
abs( ans - NowFloor ) + abs( ans - NowFromFloor )
}
其次计算人数权重:
num = InList.size() + OutList.size();
ans = (num >= 6) ? (num - 5) * 10000 : 0;
时间权重+人数权重即为该电梯的权重
锁的设计
均使用 synchronized 将共享对象锁住。这种方法的缺点是不够灵活,增加编译难度,每次使用到共享对象时便将共享对象锁住一遍,大大增加了代码量和编译难度,以及debug的难度,导致代码冗长,可读性差。
进程结束的判断
① 对生产者InputHandler 线程来说,当request == null时,读入结束,此时将托盘AllWaitList 的 IsEnd 属性赋值为true,然后结束生产者线程。
② 对调度器NowManager来说,若托盘 AllWaitList 为空,并且他的IsEnd属性为 true,则将所有电梯的 IsEnd 属性赋值为 true,然后结束调度器线程
③ 对消费者电梯线程来说,若电梯内外等待队列都为空并且 IsEnd 属性为 true,则结束电梯线程
3.bug分析
所有测评中均未出现bug
4.总结
通过本次作业的学习,我对多线程操作有了更深一步的理解,明白了生产者-消费者模型和锁的使用。同时我对调度器的设计也经过了深入的思考,并在强测中取得了不错的成绩。但缺点是我对锁的设计,每次使用到共享对象时便将共享对象锁住一遍,大大增加了代码量和编译难度,以及debug的难度,导致代码冗长,可读性差。
二、第二次作业
1、题目描述
本次作业需要模拟一个多线程实时电梯系统,该电梯系统支持动态扩展和日常维护,新增了模拟电梯系统扩建和日常维护时乘客的调度。
2.架构分析和实现
UML类图
协作图
对新增电梯的设计
首先对新增的电梯增加了满载人数和移动一层的时间两个属性,因此我对电梯类的属性又新增了Capacity 和 StopTime两个属性。同时我将所有电梯的信息保存在ArrayList Elevators 中,因此新增电梯只需要新建一个Elevator并将其放入Elevators中即可。
同时由于满载人数和移动一层的时间有所改变,调度器判断当前人员应该进入几号电梯的外部请求队列时所应用到的函数计算方法也相应做出了改变,具体来说:
时间权重:
人和电梯同方向
{
人在电梯前面或同层:StopTime * abs ( NowFloor - NowFromFloor )
人在电梯后面:
{
电梯向上:ans = max(向上的人的ToFloor, 向下的人的FromFloor)
电梯向下:ans = min(向下的人的ToFloor, 向上的人的FromFloor)
StopTime * ( abs( ans - NowFloor ) + abs( ans - NowFromFloor ) )
}
}
人和电梯反方向
{
电梯向上:ans = max(向上的人的ToFloor, 向下的人的FromFloor)
电梯向下:ans = min(向下的人的ToFloor, 向上的人的FromFloor)
StopTime * ( abs( ans - NowFloor ) + abs( ans - NowFromFloor))
}
人数权重:
num = InList.size() + OutList.size();
ans = (num >= Capacity) ? (num - Capacity + 1) * 10000 : 0;
对维护电梯的设计
电梯类新增Maintain 属性,调度器新增 MaintainNum 属性,用于判断当前还有多少个电梯正处于Maintain 状态,此属性对判断线程的结束起到重要作用。
当调度器接收到 Maintain 指令时,首先将目标电梯的 Maintain 属性赋值为 true,并且将 MaintainNum++,同时将该电梯移出电梯队列。
当某电梯的 Maintain 属性为 true 时,若该电梯的内部请求队列不为空,则进行开关门操作,并将内部请求队列里的人都放逐出电梯。然后再将外部请求队列里的人都放逐出电梯。放逐时注意,要对这些人的 ToFloor进行判断,如果 ToFloor不等于当前电梯停留的楼层的话,需要将此人再次放入托盘 AllWaitList。这时候重点来了,放入此人的时侯应当修改此人的 FromFloor 为电梯当前的楼层,否则你会发现你过了中弱测的所有点,但强测会直接寄掉。充分说明了中弱测的数据是很弱的。最后将MaintainNum–,代表此电梯完成了维护。
进程结束的判断
由于新加了 Maintain 属性,导致进程结束的判断有所不同
① 对生产者InputHandler 线程来说,当request == null时,读入结束,此时将托盘AllWaitList 的 IsEnd 属性赋值为true,然后结束生产者线程。
② 对调度器NowManager来说,若托盘 AllWaitList 为空,并且他的IsEnd属性为 true,并且 MaintainNum == 0 ,也就是说没有正在维修的电梯时,则将所有电梯的 IsEnd 属性赋值为 true,然后结束调度器线程
③ 对消费者电梯线程来说,若电梯内外等待队列都为空并且 IsEnd 属性为 true,并且 Maintain 属性为 false,则结束电梯线程
锁的设计优化
由于第一次作业时我均使用 synchronized 将共享对象锁住,导致代码冗长,可读性差。因此这次作业时我将一些方法用synchronized 锁上,减少了一些不必要的操作。但这次优化并没有时问题改变太多。
3.bug分析
本次作业顺利通过中弱测,但由于没使用测评机进行测评,导致强测只过了4个点,原因是当电梯需要Maintain,放逐某人的时候,当此人的ToFloor 不等于电梯当前停放的楼层时,没有修改此人的 FromFloor 为电梯当前的楼层。这个十分明显的bug居然可以过掉所有中弱测,可见不用测评机,强测两行泪。
同时互测也因为这个逆天bug被刀4次,再次提醒我用测评机。
4.总结
本次作业相较于第一次作业,增加的内容不是很多,同时我也意识到了测评机的重要性。有了测评机,debug变得更加轻松,让我顺利发现了bug的所在。
三、第三次作业
1、题目描述
新增电梯定制化功能,即电梯只有部分楼层可以停靠
新增电梯系统调度参数,
- 对任意楼层X,处于服务中的电梯的最大电梯数量为4
- 对任意楼层X,处于服务中的只接人的电梯的最大数量为2
2.架构分析和实现
UML类图
协作图
对电梯系统调度参数的处理
使用信号量Semaphore,代表了可以停留的电梯和可以停留的只接人的电梯的剩余数量。通过信号量的加减,各线程可以申请和释放可用资源,当没有可用资源可以申请时(此时信号量为0),线程将挂起,直到别的线程释放了该信号量对应的资源。
public void AcquireServe() {
try {
Serve.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void ReleaseServe() {
Serve.release();
}
public void AcquireOnlyInServe() {
try {
OnlyInServe.acquire();
Serve.acquire();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void ReleaseOnlyInServe() {
Serve.release();
OnlyInServe.release();
}
电梯停留时,如果是只接人的电梯,则NowManager.AcquireOnlyInServe(),否则则AcquireServe();电梯开关门操作结束之后,进行NowManager.ReleaseOnlyInServe()或者NowManager.ReleaseServe()。
对人员换乘电梯问题的处理
由于电梯只有部分楼层可以停靠,可能会出现一台电梯无法满足当前人的需求,因此可能该人需要换乘。判断该人需要先后乘坐哪些电梯则成为了问题的难点。同时考虑性能因素,我希望此人乘坐最少数量的电梯,并且每次乘坐电梯时都乘坐需要时间最短的电梯,来达到性能最佳的目的。
在换乘楼层的选择上,我采用最短路的算法。首先将1-11层楼抽象成1-11号节点,对于每台电梯,如果电梯可以从i号节点抵达j号节点,则给i和j之间连上一条权值为1的边。对每个人来说,计算该人的FromFloor到ToFloor的最短路,最短路所经过的节点编号便是该人应当停留的楼层。
在换乘电梯的选择上,依旧采用前两次作业的函数算法,分别计算可以在该人FromFloor停留的电梯的函数值,并选择最小函数值对应的电梯作为该人当前应当乘坐的电梯。
进程结束的判断
由于新加了人员换乘电梯的问题,导致进程结束的判断有所不同。将Manager类新增PersonNum,代表当前没有被送到ToFloor的所有人的数量。
① 对生产者InputHandler 线程来说,当request == null时,读入结束,此时将托盘AllWaitList 的 IsEnd 属性赋值为true,然后结束生产者线程。
② 对调度器NowManager来说,若托盘 AllWaitList 为空,并且他的IsEnd属性为 true,并且 MaintainNum == 0 ,并且PersonNum == 0,则将所有电梯的 IsEnd 属性赋值为 true,然后结束调度器线程
③ 对消费者电梯线程来说,若电梯内外等待队列都为空并且 IsEnd 属性为 true,并且 Maintain 属性为 false,则结束电梯线程
锁的设计优化
出于对第二次作业中锁方法的成功经验,本次作业我将更多方法均用 synchronized 锁住,减少锁共享对象的数量,使得代码美观性加强;缺点是容易出现死锁问题,锁的数量增加。同时我将电梯队列新建了成了一个类,来达到锁方法而不是锁对象的目的。
3.bug分析
鉴于上次作业没有使用测评机的失败,我使用了测评机进行测评,发现了bug。首先我在调度器中出现了轮询问题,导致cpu时间超时;同时我一些地方忘记加锁,导致出现一些奇怪问题。修复后成功通过测评机测评。
4.总结
本单元作业并没有经历重构,因此痛苦程度远小于第一单元。原因是我在第一次作业时就深入思考了这个问题的架构,使得代码的可拓展性提高,因此没有出现重构的问题;同时在debug方面,痛苦程度也远小于第一单元。一个原因是中弱测数据过水,导致中弱测没有发现bug,让我在国泰民安的祥和景象中安乐生活;另一个原因是测评机的合理使用,让我能更有针对性的发现问题的所在,从而更快锁定bug的位置,并成功debug。