301
社区成员
发帖
与我相关
我的任务
分享本次作业要求撰写的内容全部用📝️标出。
第二单元的任务是模拟多线程实时电梯系统,分为以下三次迭代:
一栋大楼内有多部电梯。标准输入中会不定时投放乘客请求。你需要控制电梯上下行、开关门(需花费一定时间),以及乘客进入、离开电梯,将所有乘客送往目的地,并实时输出电梯的状态。运行策略不限,但应满足运行时间等方面的限制。运行时间短、乘客等待时间短、系统耗电量小的策略更优。
在第一次作业中,所有乘客应由指定的电梯接送。
在第一次作业的基础上有两处变化:
在第二次作业的基础上,新增双轿厢电梯。普通电梯在接收到第二类重置请求后,可重置为双轿厢电梯,其中轿厢 A 可在最低层到换乘层之间运行,轿厢 B 可在换乘层到最高层之间运行,但两轿厢不可同时处于换乘层。
📝️本次作业我采用的是 Input-Queue-Elevator 架构。以下是用 IDEA 导出的类图:
| 电梯类 | 策略类 | 分配类 | 队列类 | 其他类 |
|---|---|---|---|---|
| 这些类属于普通对象,用于模拟电梯。它们只负责执行任务,不包含运行策略。 | 这些类属于线程类,每部电梯对应一个策略类对象,用于控制电梯的运行策略。 | 这些类属于线程类,它们的任务是将主队列的请求分发给各个电梯队列。 | 这些类属于共享对象,用于请求的传递。 | 这些类不属于上面任何一类,是一些杂项类。其中 Main 类也承担处理输入的功能。 |
|
|
|
|
|
📝️以下是用 draw.io 绘制的 UML 时序图(sequence diagram)。程序中每部电梯对应一个电梯队列类(ElevatorQueue)对象、一个策略类(Strategy)对象、一个电梯类(Elevator)对象,图中并未完全画出。

📝️稳定 & 易变。
📝️对于同步块的设置,我从 PPT 里择出了以下两点经验:
synchronized 代码块),而不是对整个方法做同步控制(synchronized 方法)。保证临界区最小化。事实证明,这两项设计策略是正确的——我身边有人在访问对象时进行同步控制,结果导致程序运行时……,且难以 debug。【去问问他们】
对于锁的选择,可以分为物理锁(synchronized)与逻辑锁(ReentrantLock、ReentrantReadWriteLock)。物理锁只需将整个方法锁住,而逻辑锁的使用较为麻烦,需要使用 try-finally 结构来确保产生异常时仍能释放锁,对于读写锁还要分别设置读锁和写锁。因此,在本次作业中,我一律使用物理锁。对于更大的项目,逻辑锁才能发挥其优势。
📝️在许多同学的实现中,调度策略和电梯属于同一个类。不过在我当初设计的时候,认为调度策略应该和电梯分开,电梯类只负责实现上下行、开关门、乘客进出等功能,而将具体的调度策略交给调度类实现。这样一来,就可以设计多个策略类,而电梯代码无需变动。
调度策略有很多种,常见的有 ALS 策略(性能测试的基准策略)和 LOOK 策略(更为高效,多见于学长的博客)。我最开始实现了 ALS 策略,本来希望日后能实现 LOOK 策略,但无奈没有时间了。
虽然指导书中给出了 ALS 策略的原理,但距离具体实现相差甚远。以下是指导书中提供的原理:
对于每一个电梯,都采用 ALS 策略,即新增主请求和被捎带请求两个概念
- 主请求选择规则:
- 如果电梯中没有乘客,将请求队列中到达时间最早的请求作为主请求
- 如果电梯中有乘客,将其中到达时间最早的乘客请求作为主请求
- 被捎带请求选择规则:
- 电梯的主请求存在
- 该请求投喂的时刻小于等于电梯到达该请求出发楼层关门的截止时间
- 电梯的运行方向和该请求的目标方向一致
以下是我经过反复修改、三周迭代得出的 ALS 策略实现。它现在写在 ALS 策略类的 Javadoc 里。
持续运行 ALS 调度策略,直至候乘表被标记为结束。
更新状态,直至电梯进入工作状态或候乘表被标记为结束。
每次更新状态后:
- 若电梯处于工作状态,则进行下一步。
- 否则电梯处于空闲状态,若候乘表被标记为结束则退出。
- 否则,等待候乘表变化并再次更新状态。
若有上下电梯需要:
开门。
若有重置请求:所有乘客下电梯,最后更新主请求和方向。
否则,若有下电梯需要:乘客以到达时间为顺序依次下电梯,最后更新主请求和方向。
等待动作完成。
若没有重置请求,且主请求要上电梯:主请求上电梯,并更新主请求和方向。
若没有重置请求,且有上电梯需要:乘客以到达时间为顺序依次上电梯,最后更新主请求和方向。
关门。
若有重置请求,则依次完成所有重置请求。
等待动作完成。
若电梯有运行方向:向运行方向移动一层。
等待动作完成。
其中更新状态的 Javadoc 如下:
更新状态。
- 接受所有乘客。
- 更新主请求与工作状态:
- 若有重置请求:主请求不存在,电梯处于工作状态。
- 否则,若电梯不为空:主请求为电梯中最先加入的乘客,电梯处于工作状态。
- 否则,若候乘表不为空:主请求为候乘表中最先加入的乘客,电梯处于工作状态。
- 否则:主请求不存在,电梯处于空闲状态。
- 更新电梯运行方向:
- 若主请求存在且未上电梯:运行方向指向主请求的出发楼层(若已在出发楼层则无运行方向)。
- 若主请求存在且已上电梯:运行方向指向主请求的目标楼层。
- 若主请求不存在:无运行方向。
第七次作业中引入了双轿厢。双轿厢的实现是我本单元作业中的一个败笔。
双轿厢有两种实现方法:一是将其抽象为两个电梯,两个电梯异步运行,通过共享状态来避免相撞,同时分配乘客请求时也要根据轿厢可达的楼层来分配;二是将其抽象为一种有两个轿厢的新型电梯,这两个轿厢同步运行,统一调度。分配器将请求分配给双轿厢电梯时,双轿厢电梯内部再将请求分配给两个轿厢。
可惜的是,我没有充分权衡这两种实现,就按照题目的本意——一个电梯,两个轿厢——来实现了。
这样做的结果是,我从 Elevator 类中派生出了 DoubleDeckElevator 类来实现双轿厢电梯。由于双轿厢电梯要存储两个轿厢的数据,我把每个轿厢抽象为了 Deck 类,然后用两个 Deck 对象来存储数据。……更糟糕的是,我的 ALS 策略类现在有了两套逻辑:一套应付普通电梯,一套应付双轿厢电梯,行数早就超出了 Checkstyle 规定。
于是在通过中测之后,我就开始重构。当时双轿厢电梯在用两个 Deck 对象来存储数据,而普通(单轿厢)电梯仍然直接存储数据,于是我将普通电梯重构为了使用一个 Deck 对象存储数据。接下来,我又对 ALS 策略类展开了重构,这个类重构前是 700 行,经过我的努力,终于降到了 580 行,还是没有满足 Checkstyle 的 500 行规定,但我已经没有时间了,只好作罢,最后 Checkstyle 被扣了 50 分。
仔细想来,一个电梯、两个轿厢实现上要麻烦许多,而其唯一特点——两个轿厢同步运行——却没有实在的好处。相比之下,两个电梯需要攻克的最大难关就是电梯间通信,异步运行还能节省时间。唉,真是欠考虑了。
📝️最后说一下我是如何确保双轿厢电梯的两个轿厢不会发生碰撞的:
量子电梯,作者不详。
量子电梯出现的原因,是因为题目规定电梯只需在结束移动(到达楼层)时输出信息,而开始移动时无需输出。因此,若电梯关门后一直不输出信息,则无法判断电梯是否正在移动、何时开始移动。
量子电梯的应用有二: