301
社区成员
发帖
与我相关
我的任务
分享结合三次作业,模拟多线程实时电梯系统,模拟对象是一个类似北京航空航天大学新主楼的电梯系统,楼座内有多部电梯,电梯可以在楼座内1-11层之间运行。系统从标准输入中读入乘客请求信息(起点层,终点楼层),请求调度器会根据此时电梯运行情况(电梯所在楼层,运行方向等)将乘客请求合理分配给某部电梯,然后被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。请求的输入通过我们提供的输入接口来定时投放请求,直接调用官方提供的接口即可。
可以采用任何调度策略,即任意时刻,系统选择上下行动,是否在某层开关门,乘客分配给哪部电梯都可以自定义,只要保证在电梯系统运行时间不超过题目要求时间上限的前提下将所有的乘客送至目的地即可。
接收到重置指令的电梯需要尽快停靠后完成重置动作,再投入电梯系统运行。为安全起见,电梯重置时内部不可以有乘客,且重置动作**需要时间$T_{reset}=1.2s$。
有两种重置请求:
满载人数、移动时间)。程序需要在重置完成后让电梯以新的参数运行,重置完成后,电梯处于原楼层(同上一次作业)。双轿厢电梯运行
双轿厢电梯是指在同一电梯井道内同时拥有两个独立的电梯轿厢,而电梯系统默认的普通电梯是指在一个电梯井道内只有一个轿厢。为了保证两个轿厢不相互碰撞,将楼层分为上区、下区、换乘楼层,其中上区为换乘楼层以上的所有楼层,下区为换乘楼层以下的楼层,均不包含换乘楼层。在整个运行过程中,要求轿厢 A 只能在下区和换乘楼层运行,轿厢 B 只能在上区和换乘楼层运行,同一井道内的两轿厢不能同时位于换乘楼层。请思考双轿厢电梯的优势(包括双轿厢电梯耗电量优势,具体定义见性能分说明部分),合理设计调度方案。
为避免出现多部电梯接送同一乘客造成资源浪费的情况,引入RECEIVE约束。同时我们希望同学们将RECEIVE作为调度器的附加输出来说明自己的分配方案,边界情况见正确性说明。
RECEIVE被取消当且仅当以下两种情况发生:
RECEIVE全部取消。也即[时间戳]RESET_BEGIN-电梯ID输出后,之前仍有效的RECEIVE-乘客ID-电梯ID全部取消,相关乘客处于未分配状态,电梯也视为未RECEIVE到任何乘客。RECEIVE被取消。也即[时间戳]OUT-乘客ID-所在层-电梯ID[-(A|B)]输出后,最近一个RECEIVE-乘客ID-电梯ID[-(A|B)]被取消。可参考性能分公式,由于性能分公式比较复杂,实际上公式表达出的意思就是希望你的程序在保证正确性的前提下能够尽量做到以下三点。
代码实现中不存在轮询
官方提供的输出包是线程安全的,同学们无需对其进行同步控制。
由于多线程调度存在随机性,运行产生的结果可能不同。

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

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

waitQueue —— 输入线程与调度器线程的共享对象:
同步块的设置
线程访问与更改
输入线程:
- 控制台或投喂器(指定乘坐电梯的乘客)输入请求,输入线程得到请求后,创建并初始化乘客,将其加入waitQueue中
- 控制台或投喂器(指定乘坐电梯的乘客)停止输入请求后,输入线程结束,并标记waitQueue结束。
调度器线程:
- waitQueue不为空时,取出乘客
- waitQueue为空
- 未结束 -> 等待
- 结束 -> 停止该线程
AllocatedQueues(实际的共享对象为容器内电梯的等待队列)—— 调度器线程与各电梯线程的共享对象:
同步块的设置
与waitQueue相同,为同一对象类型。
线程访问与更改
调度器线程:
- 由于乘客已指定乘坐的电梯,直接根据乘客信息加入AllocatedQueues中对应电梯的等待队列
各电梯线程(每个电梯从等待队列parallelQueue,获取调度器分配的乘客):
- parallelQueue不为空时,取出乘客
- parallelQueue为空
- 未结束 -> 等待
- 结束 -> 停止该线程
主请求切换策略(电梯内乘客队列或电梯外等待队列发生改变且两者至少有一个不为空时使用该策略)
设置主请求目的:保证电梯运行到最高层或最低层(尽量满足分配给电梯的所有乘客的需求)再调转运行方向
如何设置主请求:
- 若电梯未接到该乘客,标记电梯为未接到主请求并设置电梯向主请求出发楼层运行
- 若电梯已接到该乘客,标记电梯为已接到主请求并设置电梯向主请求目标楼层运行
只要电梯内乘客队列或电梯外等待队列至少有一个不为空,则一定存在主请求,保证电梯根据主请求改变电梯状态(电梯方向,是否接到主请求)
实现过程:
电梯内乘客序列
- 不为空
- 电梯向上运行,目标楼层最高的乘客设置为主请求
- 电梯向上运行,目标楼层最低的乘客设置为主请求
- 空:未设置主请求
电梯外等待队列
不为空:
- 若未设置主请求,从等待队列中选择到达最早的乘客作为主请求
电梯已接到主请求
乘客出发楼层与电梯所在楼层为同一楼层(已接到该乘客)
电梯向上运行,乘客目标楼层 与 现主请求目标楼层 比较,若前者较大,设置该乘客为主请求
电梯向下运行,乘客目标楼层 与 现主请求目标楼层 比较,若前者较小,设置该乘客为主请求
乘客出发楼层与电梯所在楼层不为同一楼层(未接到该乘客)
电梯向上运行,乘客的出发楼层 高于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较大值 大于 现主请的目标楼层,设置该乘客为主请求
电梯向下运行,乘客的出发楼层 低于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较小值 小于 现主请的目标楼层,设置该乘客为主请求
电梯未接到主请求
乘客出发楼层与电梯所在楼层为同一楼层(已接到该乘客)
电梯向上运行,乘客目标楼层 与 现主请求出发楼层与目标楼层的较大值 比较,若前者较大,设置该乘客为主请求
电梯向下运行,乘客目标楼层 与 现主请求出发楼层与目标楼层的较小值 比较,若前者较小,设置该乘客为主请求
乘客出发楼层与电梯所在楼层不为同一楼层(未接到该乘客)
电梯向上运行,乘客的出发楼层 高于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较大值 大于 现主请的出发楼层与目标楼层的较大值,设置该乘客为主请求
电梯向下运行,乘客的出发楼层 低于 电梯所在楼层 && 乘客的出发楼层与目标楼层的较小值 小于 现主请的出发楼层与目标楼层的较小值,设置该乘客为主请求
空:
- 不更新主请求
捎带策略
前提条件:乘客的出发楼层与电梯所在楼层一致
该乘客为主请求:保证该乘客进入电梯,若电梯已满,将电梯内乘客序列中最晚进入的乘客踢出,并根据主请求更改电梯状态
该乘客不为主请求:
未接到主请求
电梯内乘客数量小于等于电梯容量减去2,允许该乘客进入电梯
电梯内乘客数量小于等于电梯容量减去1,且该乘客能在主请求进入电梯前离开电梯(电梯在到达主请求的出发楼层前或时,该乘客已到达其目标楼层),允许该乘客进入电梯
已接到主请求:
- 电梯内乘客数量小于等于电梯容量减去1,允许该乘客进入电梯
由主类Main(主线程)初始化并创建共享对象,六部单轿厢电梯线程,调度线程,输入线程,依次启动电梯线程,调度线程,输入线程。

ResetQueue(实际的共享对象为容器内电梯的重置请求队列)—— 输入线程与各电梯线程的共享对象:
同步块的设置
CheckIfFit的方法(检查是否存在重置请求,若存在则设置电梯重置请求,返回True,否则直接返回Flase)线程访问与更改
输入线程:
- 控制台或投喂器(指定乘坐电梯的乘客)输入重置请求,输入线程得到重置请求后,创建并初始化重置请求,并将其加入ResetQueue中对应电梯的重置请求队列,并唤醒电梯线程
- 控制台或投喂器(指定乘坐电梯的乘客)停止输入请求后,输入线程结束,并标记ResetQueue结束。
电梯线程:
- 电梯在开始运行前或切换状态前都会对重置请求队列进行检查是否为空,不为空则进入重置状态,否则正常运行
Locks(实际的共享对象为容器内电梯所拥有的Lock(锁对象))—— 调度器线程与各电梯线程的共享对象:
锁的选择
lock()(加锁):若已有线程(调度器线程或者对应的电梯线程)持有锁,调用该方法的线程等待,直至前者释放锁unlock()(释放锁):释放所持有的锁,并唤醒等待的线程线程访问与更改
设置该共享对象的原因:
- 调度器线程需要依据调度策略将乘客分配给电梯,但需要保证电梯的状态不能发生改变,否则导致调度器分配乘客的目标不能实现(例如调度器依据捎带目标将乘客1分配给电梯A,但电梯A接收到乘客时,电梯A超过乘客1的出发楼层,则无法捎带),故设置该共享对象保证上述要求
调度器线程:
- 从waitQueue 获取新乘客
- 无法得到Locks中的一把锁(对应电梯正在占用Lock),等待,直到对应电梯释放Lock(电梯sleep)
- 占用Locks中所有Lock(其他电梯线程均未得到锁,不能改变状态),再依据调度策略将乘客乘客电梯
电梯线程:
- 无法得到自己的Lock,等待,直到调度器释放Lock
- 占用Lock(电梯线程的状态可能发生变化),电梯正在运行
可分配前提条件
电梯内乘客队列和电梯外等待队列的乘客数小于等于电梯容量的120%
注:若没有电梯可以分配乘客,调度器线程sleep50ms
优先级
Working,Moving状态且满足捎带策略的电梯性能指标的匹配度
由主类Main(主线程)初始化并创建共享对象,六部单轿厢电梯线程,调度线程,输入线程,依次启动电梯线程,调度线程,输入线程。
更改
保留

ShareFloor —— 双轿厢电梯的两个轿厢的共享对象:
同步块的设置
线程访问与更改
双轿厢电梯某一个轿厢准备进入换乘楼层
调用
arriveIn方法
- 若已有轿厢(另一个)位于换乘楼层,调用该方法的轿厢线程等待,直至前者离开换乘楼层
- 若未有轿厢(另一个)位于换乘楼层,设置换层楼层处于使用状态
双轿厢电梯某一个轿厢处于换乘楼层
调用
leaveOut方法
- 设置换乘楼层处于空闲状态,并唤醒等待的轿厢线程
ShareFloor保证任意时刻只有双轿厢电梯中的一个轿厢位于换乘楼层Waiting(可以保证需要使用换乘楼层的轿厢不会因另一个轿厢在换乘楼层等待导致死锁或等待时间过长)电梯启动
情景介绍:
调度线程将乘客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分析:
轿厢B因轿厢A已在换乘楼层导致不能进入,陷入等待,并且并没有释放自己的锁
调度器线程在轿厢A进行sleep时间内已拿到轿厢A的Lock,想要拿到轿厢B的Lock
轿厢A只有拿到自己的锁才能离开换乘楼层
导致死锁!!!
debug:
使轿厢B因轿厢A已在换乘楼层不能进入,陷入等待时释放自己的锁即能解决死锁
双轿厢电梯不能停止
在本代码实现当中,直接由原正常电梯直接创建双轿厢电梯,由于JVM调度的差异性,若在双轿厢电梯线程创建(原正常电梯未进入Reseting状态)前,输入线程结束,此时调度器判断所有电梯(双轿厢电梯未加入调度器所管理的电梯序列)均进入等待序列,且输入线程结束并为空,则通知目前所管理电梯线程无输入乘客,除了仍有需求需要处理的电梯线程外均停止,原双轿厢电梯能自行停止(完成双轿厢电梯时自行设定即可),但双轿厢电梯线程会进入等待调度器分配乘客序列,若未将调度器已停止的消息通知给双轿厢电梯线程,双轿厢电梯线程将一直等待,导致程序超时。
两者各有优势,前者在保证程序正常运行至结束的前提下,能且只能看到自己想看的信息(TimableOutput.println方法输出);后者只能保证程序正常运行到满足条件的位置,之后可能会由于多线程导致结果不符合期望,但是可以再中断出查看更多信息,可逐一排查。
目前,在我看来,线程安全需要保证的就是多个线程对共享对象的访问和改写的安全性。为避免出现多个线程对共享对象同时访问和改写导致的竞争问题,在本次单元学习中,主要是使用synchronized关键字和同步控制锁进行同步控制,通过合理设计,避免竞争以及上述bug分析当中出现的死锁和轮询问题。
依据UML类图,最终架构的主体部分由elevators,elevatorstate,lock,passenger,queue,reset,strategy七个包以及InputThread(输入线程),Main(主线程)组成,共有三类线程:主线程,输入线程,调度线程,电梯线程。

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