题目概况

结合三次作业,模拟多线程实时电梯系统,模拟对象是一个类似北京航空航天大学新主楼的电梯系统,楼座内有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息(起点层,终点楼层),请求调度器会根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。请求的输入通过我们提供的输入接口来定时投放请求,直接调用官方提供的接口即可。

可以采用任何调度策略,即任意时刻,系统选择上下行动,是否在某层开关门,乘客分配给哪部电梯都可以自定义,只要保证在电梯系统运行时间不超过题目要求时间上限的前提下将所有的乘客送至目的地即可。


电梯重置

接收到重置指令的电梯需要尽快停靠后完成重置动作,再投入电梯系统运行。为安全起见,电梯重置时内部不可以有乘客,且重置动作**需要时间$T_{reset}=1.2s$。

两种重置请求:

  1. 第一类重置请求仅修改电梯参数,重置参数请求包含需要重置的电梯ID和电梯相关参数(满载人数移动时间)。程序需要在重置完成后让电梯以新的参数运行,重置完成后,电梯处于原楼层(同上一次作业)。
  2. 第二类重置请求将电梯修改为双轿厢电梯,重置参数包含需要重置的电梯ID,换乘楼层,两个轿厢的相关参数(移动一层的时间和满载人数)相同。当重置完成后,轿厢 A 默认初始在换乘楼层的下面一层,轿厢B默认初始换乘楼层的上面一层。(本次新增)

双轿厢电梯运行

​ 双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢,而电梯系统默认的普通电梯是指在一个电梯井道内只有一个轿厢。为了保证两个轿厢不相互碰撞,将楼层分为上区、下区、换乘楼层,其中上区为换乘楼层以上的所有楼层,下区为换乘楼层以下的楼层,均不包含换乘楼层。在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层。请思考双轿厢电梯的优势(包括双轿厢电梯耗电量优势,具体定义见性能分说明部分),合理设计调度方案。


RECEIVE约束

为避免出现多部电梯接送同一乘客造成资源浪费的情况,引入RECEIVE约束。同时我们希望同学们将RECEIVE作为调度器的附加输出来说明自己的分配方案,边界情况见正确性说明

RECEIVE被取消当且仅当以下两种情况发生:

  • 电梯开始重置后,其之前与此电梯有关的RECEIVE全部取消。也即[时间戳]RESET_BEGIN-电梯ID输出后,之前仍有效的RECEIVE-乘客ID-电梯ID全部取消,相关乘客处于未分配状态,电梯也视为未RECEIVE到任何乘客
  • 乘客中途走出电梯后,相应的RECEIVE被取消。也即[时间戳]OUT-乘客ID-所在层-电梯ID[-(A|B)]输出后,最近一个RECEIVE-乘客ID-电梯ID[-(A|B)]被取消。

性能分说明

可参考性能分公式,由于性能分公式比较复杂,实际上公式表达出的意思就是希望你的程序在保证正确性的前提下能够尽量做到以下三点。

  • 运行时间尽量短
  • 尽量不让请求等待过长时间
  • 尽量减少系统的无效运行

说明与警示

  • 代码实现中不存在轮询
  • 官方提供的输出包是线程安全的,同学们无需对其进行同步控制。

  • 由于多线程调度存在随机性,运行产生的结果可能不同。

设计分析

  • UML类图

Unit_2

最终架构的主体部分由elevators,elevatorstate,lock,passenger,queue,reset,strategy七个包以及InputThread(输入线程),Main(主线程)组成,共有三类线程:主线程,输入线程,调度线程,电梯线程。

  • UML协作图

image-20240420152512401

第一次作业

由主类Main(主线程)初始化并创建共享对象,六部单轿厢电梯线程,调度线程,输入线程,依次启动电梯线程,调度线程,输入线程。

需求

  • 指定乘客乘坐的电梯,不需要调度策略
  • 输出符合输出格式:电梯与乘客状态输出
  • 整个过程所有电梯的状态都要符合正确性说明
  • 避免说明与警示出现的问题:轮询,初始化时间戳
  • 尽量符合性能指标(运行时间,请求等待时间,耗电量)使电梯运行

状态模式

OO_Unit2_First

共享对象

  • waitQueue

    介绍:输入线程与调度器线程的共享对象

    同步块的设置

    • 对共享对象内的属性进行访问与更改(设置结束,取出乘客,是否为空等)的方法均加上synchronized关键字,进行同步控制
    • 取出乘客与增加乘客方法,两者需要使用wait()和notifyAll()进行同步控制
      • 调度器线程调用取出一个乘客的方法,若队列为空则进行等待,若不为空则直接取出乘客
      • 输入线程调用增加乘客的方法,向队列中增加乘客,并唤醒等待的调度器线程

    线程访问与更改

    输入线程:

    • 控制台或投喂器(指定乘坐电梯的乘客)输入请求,输入线程得到请求后,创建并初始化乘客,将其加入waitQueue中
    • 控制台或投喂器(指定乘坐电梯的乘客)停止输入请求后,输入线程结束,并标记waitQueue结束。

    调度器线程:

    • waitQueue不为空时,取出乘客
    • waitQueue为空
      • 未结束 -> 等待
      • 结束 -> 停止该线程
  • AllocatedQueues

    介绍:实际的共享对象为容器内电梯的等待队列—— 调度器线程与各电梯线程的共享对象:

    同步块的设置

    与waitQueue相同,为同一对象类型。

    • 各电梯线程调用取出全部乘客的方法,若队列为空则进行等待,若不为空则直接取出全部乘客
    • 调度器线程调用增加乘客的方法,向队列中增加乘客,并唤醒等待的电梯线程

    线程访问与更改

    调度器线程:

    • 由于乘客已指定乘坐的电梯,直接根据乘客信息加入AllocatedQueues中对应电梯的等待队列

    各电梯线程(每个电梯从等待队列parallelQueue,获取调度器分配的乘客):

    • parallelQueue不为空时,取出乘客
    • parallelQueue为空
      • 未结束 -> 等待
      • 结束 -> 停止该线程

运行策略

  • 主请求切换策略

    • 策略场景:电梯内乘客队列或电梯外等待队列发生改变且两者至少有一个不为空时使用该策略

    • 设置主请求目的:保证电梯运行到最高层或最低层(尽量满足分配给电梯的所有乘客的需求)再调转运行方向

    • 如何设置主请求:

      • 若电梯未接到该乘客,标记电梯为未接到主请求并设置电梯向主请求出发楼层运行
      • 若电梯已接到该乘客,标记电梯为已接到主请求并设置电梯向主请求目标楼层运行
    • 只要电梯内乘客队列或电梯外等待队列至少有一个不为空,则一定存在主请求,保证电梯根据主请求改变电梯状态(电梯方向,是否接到主请求)

    • 实现过程:

      • 电梯内乘客序列

        • 不为空
          • 电梯向上运行,目标楼层最高的乘客设置为主请求
          • 电梯向上运行,目标楼层最低的乘客设置为主请求
        • 空:未设置主请求
      • 电梯外等待队列

        • 不为空:

          • 若未设置主请求,从等待队列中选择到达最早的乘客作为主请求

          电梯已接到主请求

          乘客出发楼层与电梯所在楼层为同一楼层(已接到该乘客)

          • 电梯向上运行,乘客目标楼层 与 现主请求目标楼层 比较,若前者较大,设置该乘客为主请求

          • 电梯向下运行,乘客目标楼层 与 现主请求目标楼层 比较,若前者较小,设置该乘客为主请求

          乘客出发楼层与电梯所在楼层不为同一楼层(未接到该乘客)

          • 电梯向上运行,乘客的出发楼层 高于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较大值 大于 现主请的目标楼层,设置该乘客为主请求

          • 电梯向下运行,乘客的出发楼层 低于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较小值 小于 现主请的目标楼层,设置该乘客为主请求

          电梯未接到主请求

          乘客出发楼层与电梯所在楼层为同一楼层(已接到该乘客)

          • 电梯向上运行,乘客目标楼层 与 现主请求出发楼层与目标楼层的较大值 比较,若前者较大,设置该乘客为主请求

          • 电梯向下运行,乘客目标楼层 与 现主请求出发楼层与目标楼层的较小值 比较,若前者较小,设置该乘客为主请求

          乘客出发楼层与电梯所在楼层不为同一楼层(未接到该乘客)

          • 电梯向上运行,乘客的出发楼层 高于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较大值 大于 现主请的出发楼层与目标楼层的较大值,设置该乘客为主请求

          • 电梯向下运行,乘客的出发楼层 低于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较小值 小于 现主请的出发楼层与目标楼层的较小值,设置该乘客为主请求

        • 空:

          • 不更新主请求
  • 捎带策略

    前提条件:乘客的出发楼层与电梯所在楼层一致

    • 该乘客为主请求:保证该乘客进入电梯,若电梯已满,将电梯内乘客序列中最晚进入的乘客踢出,并根据主请求更改电梯状态

    • 该乘客不为主请求:

      • 未接到主请求

        • 电梯内乘客数量小于等于电梯容量减去2,允许该乘客进入电梯

        • 电梯内乘客数量小于等于电梯容量减去1,且该乘客能在主请求进入电梯前离开电梯(电梯在到达主请求的出发楼层前或时,该乘客已到达其目标楼层),允许该乘客进入电梯

      • 已接到主请求:

        • 电梯内乘客数量小于等于电梯容量减去1,允许该乘客进入电梯

第二次作业

由主类Main(主线程)初始化并创建共享对象,六部单轿厢电梯线程,调度线程,输入线程,依次启动电梯线程,调度线程,输入线程。

需求

  • 更改

    • 不指定乘客乘坐的电梯,需要调度策略
    • 电梯重置,设置电梯的容量和运行速度
    • 实现RECEIVE约束
    • 输出符合输出格式:增加重置输出与RECEIVE输出
  • 保留

    • 整个过程所有电梯的状态都要符合正确性说明
    • 避免说明与警示出现的问题
    • 尽量符合性能指标使电梯运行

状态模式

OO_Unit2_Second

共享对象(增加)

  • ResetQueue

    介绍:实际的共享对象为容器内电梯的重置请求队列—— 输入线程与各电梯线程的共享对象:

    同步块的设置

    • 对共享对象内的属性进行访问与更改(设置结束,取出重置请求,是否为空等)的方法均加上synchronized关键字,进行同步控制
    • 取出重置请求与增加重置请求方法,仅使用synchronized关键则进行同步控制即可
      • 各电梯线程调用CheckIfFit的方法(检查是否存在重置请求,若存在则设置电梯重置请求,返回True,否则直接返回Flase
      • 输入线程调用增加重置请求的方法,向队列中增加重置请求

    线程访问与更改

    输入线程:

    • 控制台或投喂器(指定乘坐电梯的乘客)输入重置请求,输入线程得到重置请求后,创建并初始化重置请求,并将其加入ResetQueue中对应电梯的重置请求队列,并唤醒电梯线程
    • 控制台或投喂器(指定乘坐电梯的乘客)停止输入请求后,输入线程结束,并标记ResetQueue结束。

    电梯线程:

    • 电梯在开始运行前或切换状态前都会对重置请求队列进行检查是否为空,不为空则进入重置状态,否则正常运行
  • Locks

    介绍:实际的共享对象为容器内电梯所拥有的Lock(锁对象)—— 调度器线程与各电梯线程的共享对象

    锁的选择

    • Lock为自行实现的同步控制锁,每部电梯线程均有自己的锁并与调度器线程共享,调度器线程通过Locks对象对所有电梯的锁进行统一管理。
    • lock()(加锁):若已有线程(调度器线程或者对应的电梯线程)持有锁,调用该方法的线程等待,直至前者释放锁
    • unlock()(释放锁):释放所持有的锁,并唤醒等待的线程

    线程访问与更改

    设置该共享对象的原因:

    • 调度器线程需要依据调度策略将乘客分配给电梯,但需要保证电梯的状态不能发生改变,否则导致调度器分配乘客的目标不能实现(例如调度器依据捎带目标将乘客1分配给电梯A,但电梯A接收到乘客时,电梯A超过乘客1的出发楼层,则无法捎带),故设置该共享对象保证上述要求

    调度器线程:

    • 从waitQueue 获取新乘客
      • 无法得到Locks中的一把锁(对应电梯正在占用Lock),等待,直到对应电梯释放Lock(电梯sleep)
      • 占用Locks中所有Lock(其他电梯线程均未得到锁,不能改变状态),再依据调度策略将乘客乘客电梯

    电梯线程:

    • 无法得到自己的Lock,等待,直到调度器释放Lock
    • 占用Lock(电梯线程的状态可能发生变化),电梯正在运行

运行策略 —— 不变

调度策略

  • 可分配前提条件

    电梯内乘客队列和电梯外等待队列的乘客数小于等于电梯容量的120%

    注:若没有电梯可以分配乘客,调度器线程sleep50ms

  • 优先级

    1. 处于Working,Moving状态且满足捎带策略的电梯
    2. 电梯内乘客队列和电梯外等待队列的乘客数为零 且 距离乘客出发楼层最近的电梯
    3. 电梯内乘客数量最少的电梯
  • 性能指标的匹配度

    • 前提条件:避免出现将乘客均分配给一个电梯的情况,避免分配给电梯的乘客超出电梯的处理能力范围,否则会出现不符合性能指标的现象
    • 最高优先级:保证乘客尽量被捎带,避免分配给空闲电梯,增加耗电量,同时能将乘客的等待时间控制在一定限度内(电梯运行一轮次的时间)
    • 第二优先级:避免乘客未能匹配到能被捎带的电梯,防止等待时间过长
    • 第三优先级:优选最近的电梯,尽量保证乘客能分配给电梯,减少等待时间
    • 程序运行时间与乘客等待时间密切(正)相关,关注后者即可

第三次作业

由主类Main(主线程)初始化并创建共享对象,六部单轿厢电梯线程,调度线程,输入线程,依次启动电梯线程,调度线程,输入线程。

需求

更改

  • 增加电梯重置:单轿厢电梯重置为双轿厢电梯
  • 输出符合输出格式:重置双轿厢电梯后,电梯与乘客状态输出格式发生变化
  • 尽量符合性能指标使电梯运行:双轿厢电梯耗电量为以上数据的$\frac{1}{4}$

保留

  • 不指定乘客乘坐的电梯,需要调度策略
  • 电梯重置,设置电梯的容量和运行速度
  • 实现RECEIVE约束
  • 整个过程所有电梯的状态都要符合正确性说明
  • 避免说明与警示出现的问题

状态模式

  • 状态模式与第二次作业相同,但需控制双轿厢电梯的两个轿厢不发生碰撞

OO_Unit2_Second

共享对象(增加)

  • ShareFloor

    介绍:双轿厢电梯的两个轿厢的共享对象

    同步块的设置

    • 对共享对象内的属性进行访问与更改(进入换乘楼层,离开换乘楼层等)的方法均加上synchronized关键字,进行同步控制

    线程访问与更改

    双轿厢电梯某一个轿厢准备进入换乘楼层

    调用arriveIn方法

    • 若已有轿厢(另一个)位于换乘楼层,调用该方法的轿厢线程等待,直至前者离开换乘楼层
    • 若未有轿厢(另一个)位于换乘楼层,设置换层楼层处于使用状态

    双轿厢电梯某一个轿厢处于换乘楼层

    调用leaveOut方法

    • 设置换乘楼层处于空闲状态,并唤醒等待的轿厢线程

双轿厢电梯碰撞控制

  1. 通过上述共享对象ShareFloor保证任意时刻只有双轿厢电梯中的一个轿厢位于换乘楼层
  2. 双轿厢电梯中的一个轿厢若位于换乘楼层,将需要换乘(目的楼层超出本轿厢电梯的运行范围)的乘客踢出电梯
    • 其内的乘客队列与其外的等待队列为空,先操作电梯离开换乘楼层,再设置电梯状态为Waiting(可以保证需要使用换乘楼层的轿厢不会因另一个轿厢在换乘楼层等待导致死锁或等待时间过长)
    • 其内的乘客队列与其外的等待队列不为空,继续正常运行

Bug分析

第一次作业

  • 电梯启动

    情景介绍:

    调度线程将乘客1分配给电梯线程A,电梯A需改变Waiting状态,该乘客为电梯A此时的主请求,若乘客1楼层与电梯A所在楼层位于同一层,接到主请求,电梯随后进入Working状态;若乘客1楼层与电梯A所在楼层不位于同一层,未接到主请求,电梯随后进入Moving状态;

    根据具体实现,电梯再切换到下一个状态前,需先判断主请求状态。在上述情景中,假设乘客1楼层与电梯A所在楼层位于同一层,判断接到主请求,将乘客1移出等待队列并在随后的Working状态输出乘客进入电梯。在Working状态开关门时间内,调度器线程可能会分配多于电梯1容量的乘客给电梯1,导致电梯1可能在本楼层需要接送的乘客超出容量。

    bug分析:

    本楼层需接送乘客过多,主请求未能进入电梯,bug产生!

    debug:

    提前设定特判条件保证上述主请求最先进入电梯,

  • 电梯移动时的主请求切换

    情景介绍:

    在电梯移动过程中,进行主请求切换后,若未接到主请求,需要保证电梯到达主请求所在楼层时,使主请求进入电梯

    bug分析:

    在上述情景下,可能由于电梯已满,导致主请求未能进入电梯,bug产生!

    debug:

    电梯已满的情况下,将最晚进入电梯的乘客踢出,保证主请求进入电梯

第二次作业

  • 轮询

    情景介绍:

    输入线程因输入停止结束线程,调度器线程的待分配乘客队列仍不为空(未分配出去),调度器线程终止的条件为输入线程终止 && 待分配乘客队列为空 && 所有电梯均处于等待状态(避免电梯仍在运行时踢出乘客),调度器线程从共享对象WaitQueue中取出乘客时的等待条件输入线程未终止 && 待分配乘客队列为空。

    bug分析:

    上述情景下,导致调度器线程从共享对象WaitQueue中取出乘客时,由于输入线程已终止,不会再进行等待,导致轮询

    debug:

    将调度器线程从共享对象WaitQueue中取出乘客时的等待条件待分配乘客队列为空

第三次作业

  • 死锁

    情景介绍:

    双轿厢电梯中的轿厢A已位于换乘楼层,已完成开关门,准备拿到自己的Lock继续运行,轿厢B准备进入换乘楼层。

    每个电梯在进入sleep前均要释放自己的Lock(其作用已解释),允许调度器线程拿到所有电梯的Lock再根据调度策略分配乘客,然后电梯再sleep结束后要拿到自己的Lock,保证电梯再运行阶段调度器线程不会再分配乘客(调度器线程只有拿到所有电梯的Lock方能分配乘客,完成一次分配乘客再一次性释放所有锁)。

    双轿厢电梯中的一个轿厢想要进入换乘楼层,需要保证无轿厢在换乘楼层

    bug分析:

    1. 轿厢B因轿厢A已在换乘楼层导致不能进入,陷入等待,并且并没有释放自己的锁

    2. 调度器线程在轿厢A进行sleep时间内已拿到轿厢A的Lock,想要拿到轿厢B的Lock

    3. 轿厢A只有拿到自己的锁才能离开换乘楼层

    导致死锁!!!

    debug:

    使轿厢B因轿厢A已在换乘楼层不能进入,陷入等待时释放自己的锁即能解决死锁

  • 双轿厢电梯不能停止

在本代码实现当中,直接由原正常电梯直接创建双轿厢电梯,由于JVM调度的差异性,若在双轿厢电梯线程创建(原正常电梯未进入Reseting状态)前,输入线程结束,此时调度器判断所有电梯(双轿厢电梯未加入调度器所管理的电梯序列)均进入等待序列,且输入线程结束并为空,则通知目前所管理电梯线程无输入乘客,除了仍有需求需要处理的电梯线程外均停止,原双轿厢电梯能自行停止(完成双轿厢电梯时自行设定即可),但双轿厢电梯线程会进入等待调度器分配乘客序列,若未将调度器已停止的消息通知给双轿厢电梯线程,双轿厢电梯线程将一直等待,导致程序超时。

Debug方法

print大法

  • 根据bug推测其可能的位置,利用TimableOutput.println方法,获取bug附近尽可能多的信息,推测bug产生的原因

调试,设置条件断点

  • 根据bug推测其可能的位置,在该位置设置条件断点,保证程序在运行到该条件断点前不被打断,模拟程序正常运行的场景;满足条件方可中断,再查看当前各线程的状态即可

两者各有优势,前者在保证程序正常运行至结束的前提下,能且只能看到自己想看的信息(TimableOutput.println方法输出);后者只能保证程序正常运行到满足条件的位置,之后可能会由于多线程导致结果不符合期望,但是可以再中断出查看更多信息,可逐一排查。

心得体会

线程安全

目前,在我看来,线程安全需要保证的就是多个线程对共享对象的访问和改写的安全性。为避免出现多个线程对共享对象同时访问和改写导致的竞争问题,在本次单元学习中,主要是使用synchronized关键字和同步控制锁进行同步控制,通过合理设计,避免竞争以及上述bug分析当中出现的死锁和轮询问题。

层次化设计

依据UML类图,最终架构的主体部分由elevators,elevatorstate,lock,passenger,queue,reset,strategy七个包以及InputThread(输入线程),Main(主线程)组成,共有三类线程:主线程,输入线程,调度线程,电梯线程。

Unit2_层次化设计

依托图中的层次化设计,只需关注各层次内部的准确性以及各层次之间的关联性,分步完成各层次即可,同时方便定位bug。