304
社区成员
发帖
与我相关
我的任务
分享U2电梯调度,主要通过Java多线程模式完成多部电梯的调度策略。
我们需要根据需求与正确性约束,设计多线程程序来模拟该电梯系统的运行。系统从标准输入中读入乘客请求信息,请求调度器会根据此时电梯运行状态将乘客请求合理分配给某部电梯。被分配请求的电梯会经过上下行,开关门,乘客进入/离开电梯等动作将乘客从起点层运送到终点层。
三次迭代,从最初hw5的指定单电梯运行模式,到hw6、hw7需要自己完成电梯的指派以及临时检修、双轿厢升级等任务,整体架构一步步升级,也在不断地锻炼着我们的多线程设计能力。
锁和同步块,根本上是限制多线程状态下对同一数据读写的互斥访问。
hw5:采用 synchronized 锁与 wAIt/notifyAll 机制实现生产者-消费者模式。同步块通过保护共享队列(ScheduleQueue 和 ElevatorQueue)中的请求列表及结束标志,确保了乘客任务存取的原子性,并利用线程阻塞机制替代了轮询。
hw6:延续了基于 synchronized 的锁机制,主要保护 ScheduleQueue 和 ElevatorQueue。变化在于引入了 activeCount 和检修状态位。锁不仅保护任务存取,还维持了全系统生命周期控制。ScheduleQueue 的 poll 方法不仅检查结束标志,还需判断 activeCount 是否清零。在 DispatchThread 中,同步块用于阻塞调度尝试,实现了线程间复杂的协作同步。
hw7:除了延续对 ScheduleQueue 和 ElevatorQueue 的保护外,新增了两个核心同步对象:
UpdateRecycleController:通过 synchronized 保护电梯井的状态机(ShaftMode),确保主备轿厢在改造与回收过程中状态切换的原子性。TransferFloorCoordinator:这是解决双轿厢冲突的关键。利用 wAIt/notifyAll 机制控制主备轿厢对换乘层(F2)的互斥访问。同步块内部逻辑严格遵守对应约束。例如在移动前,电梯必须持有协作锁检查目标层是否被同一井道的另一轿厢占用,若冲突则进入 wAIt,确保了电梯不相撞。
三次作业都是以调度器(DispatchThread)作为中转核心,构建了生产者-消费者链条:它负责从全局队列提取请求并根据调度策略分发至对应电梯的私有队列。这种设计实现了输入与执行的解耦,使得任务派发更加清晰。
hw5没什么要说的,主要是搭好了调度器的框架,为hw6和hw7做准备。
hw6,调度器通过 DispatchSnapshot 与各电梯线程交互。
调度器在决策前,通过快照机制获取各电梯的实时状态,而无需长时间锁定电梯对象。针对临时检修,调度器与 MAIntController 协同:当电梯进入检修,调度器将其从候选名单剔除;检修抛出的未完成请求被重新塞回 ScheduleQueue。这种设计实现了任务的动态重分配。
hw7,调度器负责处理乘客、检修、改造及回收四种请求:
调度器与 UpdateRecycleController 耦合。在分发乘客请求前,它会先查询井道模式。若井道处于改造中,则阻塞该井道的派发;若处于双轿厢模式,则根据乘客起终点判断该由主轿厢还是备轿厢接收。
对于 UpdateRequest 和 RecycleRequest,调度器通过LifecycleCommand异步下发。电梯线程在执行动作的间隙检查该命令,从而在满足 6-7 秒响应时限的同时,平滑地完成乘客清空与任务移交。
主要针对于hw6和hw7。
hw6引入了影子电梯调度策略,通过ShadowElevatorManager贪心模拟将新请求分配给不同电梯后的演变过程,基于性能分计算公式来多维度适配性能指标:
ARRIVE和 OPEN/CLOSE动作,计算能量损耗。这有效抑制了电梯频繁为了单个远距离乘客而奔波,倾向于“捎带”以节省能耗。hw7相较于hw6变化不大,主要是增加了换乘惩罚分。核心调度策略还是基于影子电梯来完成。
出现过的bug:
hw5未发现bug。
hw6强测正常,互测时突然想到了【5部电梯同时维修从而只能将乘客请求全部分派给1部电梯】的情况,于是互测一穿五了。当然如果能hack自己的话也就把自己hack了,虽然后来也有人发现了这个数据。
修bug的方法很简单,限制一个电梯最多同时持有的乘客请求数量,达到上限后不再分派就好了。
hw7强测正常,互测时被测出了升级/回收超时的bug。主要原因是忘记单独处理检修和回收请求,导致它们可能和乘客请求在高并发的情况下一同被阻塞,从而无法即使响应升级/回收请求。
debug方法:
多线程与单线程的程序极为不同。
单线程程序的运行状态基本是固定的,同一时刻只会有一条被执行的指令,这使得debug及其方便,因为我们可以逐条地执行代码来观察程序各部分的演变过程。
但多线程程序则完全无法做到这一点。所以我的debug主要靠自己搭建的评测机来辅助完成。通过手动构造数据并运行来观察程序运行结果。这种方法是相对快捷且有效的方法了。
原子性与可见性的基础:
也就是保护共享资源。例如 ScheduleQueue 和 ElevatorQueue。通过对这些容器加锁,保证了多个生产者(InputThread, DispatchThread)和消费者(ElevatorThread)在存取请求时不会出现数据撕裂。这是最底层的安全。
生命周期与信号丢失:
随着作业引入“检修”和“双轿厢改造”,线程安全进入了状态管理阶段。
activeCount(活跃请求计数)是一种高级的线程安全策略,它保证了信号的完整性,防止电梯在任务还没重排完时就结束。wAIt/notifyAll 建立的阻塞唤醒机制,不仅保护了数据,更保护了 CPU 资源。空间冲突与物理约束:
HW7 的双轿厢冲突是线程安全最高级的体现。此时“锁”的对象不再只是一个变量,而是物理空间。
TransferFloorCoordinator 实际上是一个锁管理器。它通过协作协议保证了两个线程在操作各自的 curFloor 时,逻辑上永远不会重叠。这种安全比单纯的代码加锁更依赖精巧的设计。层次化设计是应对复杂性的有效方式。通过三次迭代,系统结构逐渐清晰:
管路分层:Input -> Dispatcher -> Elevator
InputThread): 仅仅负责解析,不参与任何逻辑。DispatchThread): 不直接操作电梯的升降,只负责信息的流转和决策(影子模拟)。它通过 DispatchSnapshot(快照)观察电梯,这种设计极大降低了层间耦合。ElevatorThread): 它只关心自己队列里的任务,并通过 MAIntController 或 UpdateRecycleController 处理复杂的特殊动作。职责分离(解耦的核心):
resolveLookDir 计算出的方向。Controller 中。如果把这些逻辑全部塞进 ElevatorThread,代码将变成不可维护的“上帝类”。现在的设计中,电梯线程只需在循环中判断,而具体的执行由 Controller 代理。影子模拟:
HW6/HW7 的影子模拟是层次化设计的典型体现。它在调度层建立了一个虚拟执行层。通过提取电梯的快照,在内存中进行平行模拟。这证明了只要各层接口定义清晰,上层可以完全模拟下层的行为而互不干扰。
良好的层次化设计会天然减少线程安全问题的发生。当每一层只负责单一职责,且层间通过定义的共享容器通信时,需要加锁的边界变得非常清晰。
我在三次强测中的性能分表现都不错,简单总结一下我的优化策略。
首先,对于单电梯,采用LOOK运行策略。这也是课程组推荐的综合性能优秀的单电梯运行策略。
在LOOK的基础上,通过阅读往届学长的博客,我了解到了“量子电梯”的优化策略。以下是我对量子电梯的理解:
之所以叫做“量子”,是因为我们假想电梯可能会处在一种“量子叠加态”。具体来说,我们可以发现我们作业中的电梯没有在真正的“移动”,而是在通过输出ARRIVE来模拟“到达”。也就是说,电梯在从这一层向下一层的移动的这0.4秒过程中,我们不知道电梯到底有没有在“移动”,电梯会处在“停在这一层”和“向下一层移动中”这两种状态的叠加态。而在这0.4秒内,如果在本层来了乘客请求,那么电梯坍缩为“停在这一层”的状态,我们就可以完成在本层的开门接客;如果没有请求,那么0.4秒后,电梯会直接输出ARRIVE下一层的信息,电梯坍缩为“向下一层移动”。
这种处理方式能够在不违反指导书电梯运行约束的情况下尽可能榨干单电梯的运行性能,而且实现起来并不复杂,十几行代码即可解决。
然后,对于调度策略,同样地通过学习往届学长的经验,我采取了“影子电梯”调度策略。
我理解的“影子电梯”,就是对未来的“模拟”。对于一个新来的乘客请求,我们分别模拟【把这个乘客分别分派给各个电梯】,在【假设接下来不会再有新乘客请求】的情况下,模拟【接下来整个电梯系统会如何运行】,并根据各份模拟结果,选择能让性能更优的电梯进行真正的分派。
往届的模拟好像大多数只考虑了使电梯运行结束地尽可能早,而我出于对性能分的考虑,也加上了对电梯耗能的考虑。最后综合考虑多个性能指标进行打分,选择最合适的那个电梯分派。
最后,对于检修/升级/回收正式开始前的预准备阶段,我的单电梯运行策略比较激进:
在接收到检修/升级/回收指令后,让电梯依旧完全按照原有的LOOK来运作。不过电梯在做所有动作之前,都要进行这样的判断:
【若我从当前的状态,去下一个我原本想要停靠的楼层,完成开门关门,之后直奔F1完成检修,会不会导致最终时长超过约束?】
若会超过时长约束,则我们看看能不能将一些乘客在当前这层放下去,如果可以,就放;如果不能,直接跑回F1(若轿厢里面有乘客,则全部一起运到F1踢下去)。若不会超过时长约束,那我们就依旧去下一个停靠层。
不难发现,电梯下一步要去几层,仅与电梯的等待队列的乘客请求和电梯内的乘客请求有关。那么可以这样做:当电梯在某层开门,把该下的乘客下了之后,要根据LOOK策略决定上客和接下来的运行策略之前,遍历【该电梯内】和【该层该电梯外】的所有的当前的乘客请求.
对于某个乘客请求,计算:{该电梯在“如果我接下来仅载这一个乘客”的情况下}计算出对应的“可服务下一站”,若电梯根据原有判断规则认为会超时限,则该电梯【一定不会】服务这个乘客。这样的情况下,我们直接让这位乘客IN电梯(如果他原本在电梯外),再紧接着直接OUT-F,这样做可以最快地放弃该乘客的请求,尽快让别的电梯接收该乘客的请求。若电梯根据原有判断规则认为不会超时限,则我们可能会服务完该乘客,我们先暂时不用管他。经过这样一次筛选,我们可能会把一部分请求通过OUT-F的方式给释放掉。那么对于筛选后剩下的乘客,我们正常地进入原先的逻辑(即原有的LOOK策略)。
最后,为保证整体性能,添加硬性约束:只要电梯运行到了计划层,无论是经过还是说本来就打算停在计划层,那么直接准备开始正式的维修/升级/回收,不再管乘客了。
除此之外,具体的实现中还有更多细粒度的优化策略。我认为上述的这些策略虽然并不一定能保证最优,但综合情况下会比较优秀。
在上一单元的博客中,我曾希望我在U2中能够尽量少地使用AI。不过由于各种原因(主要是中间穿插的冯如杯实在太耗时间),我本单元对AI的使用程度没有减少太多,但是还是有调整了一下AI的使用方式的。同样地,我依然会在继续尽量在接下来的单元中尽可能减少使用AI辅助编程。
我对AI的使用主要是让它辅助实现我的一些想法。正如前面所述,具体的优化策略AI很难提供正确的方向,所以各种优化策略基本都是我经过反复试错、构造边界情况等方式,综合考虑各方面后最终想出的。但从前面我所说的优化策略可以看出,我的策略的复杂度确实不算低,奈何我想到的优化策略并不好实现,而我的时间和能力亦不足以很好地支持我把我的想法落实到代码上。
所以在实现一些复杂策略时,我会首先尽可能详细地描述清楚我的想法,避免AI出现理解上的偏差。然后与AI交流实现上的可能性、可行性、鲁棒性等。最后确定具体的改进策略,让AI辅助我完成代码的修改。我认为这种方式能够较好地平衡人与AI的参与程度。
不过我也明显感受到了AI的局限性。
首先,我并不能完全保证我提出的优化策略是合理且正确的,而AI天然地倾向于顺从用户的想法,往往不能很好地发现我的想法中存在的bug。
其次,即使我的优化策略基本正确,且基本描述清楚。但在交给AI时,AI难免出现理解偏差,不能完全按照我的想法来做,总是会加上一些自己的想法从而写出bug。
然后,当我发现某个数据在运行时会出现bug时,由于瞪眼找bug实在太过费劲,所以我在不少时候也会借助AI来完成。但是AI改这些bug时往往只会“打补丁”,很多时候只会在现有代码的基础上添加十几行几十行的修补代码,难以发现其底层的根因。这种情况在代码量上升后尤为明显。我一般会在它给我打完补丁后复盘一下它的代码,往往能够发现很多冗余代码或逻辑,需要我手动完善修改或再次给它指出后让它修改。AI这种打补丁的修改方式是我的代码量明显偏多的重要原因。
这个问题也是我感受到的AI的最大弱点之一。我设想了这样一个情景:
想象你在马路上正用着自动驾驶开着车,但前面突然正在修路,路被封了一部分,但没有封死。你看到前面一个个车压着马路牙子顺利地开过去了,但是你的AI却自顾自地调头,为你切换了一条备选路线,你绕一大圈才能回到原有的路线上。因为它被设计时就并不允许在马路牙子上开。
你很急,你气愤地质问它“你是猪吗,你就不能也压着马路牙子过去吗”。然后AI给你说了压马路牙子的三个危害,首先它会...,其次还会...,最后也可能...。它说其实还有折中方案,你一听很高兴,但它却说“我们或许可以把施工队赶走,或者再旁边再开一条路出来”。
你没招了,你想亲自接管你的车。但这时候问题来了:要是你也不会在马路牙子上开呢?最终你还是急头白脸地接受了AI一开始提出的备选路线。走了一段突然发现,AI认为那条路已经不能走了,所以已经切换到了一条全新的路线,要不要我给你生成一版原有路线与新版路线的对照图?
总的来说,使用感受和我在第一单元的使用感受比较相近,也就是说,对于我们的作业而言,【AI姑且能保证正确性】,但是【很难自己找出 或者说创造出 合理的优化策略】。这是AI目前在面对oo的这些任务时明显欠缺的点。
另外,与第一单元一样,我同样借助AI开发了简易的评测机。我的评测机能够实现【手动输入单次运行】、【批量运行代码】、【严格判断输出正确性】、【基于输入输出展示图形化界面模拟电梯系统运行过程】、【自动批量生成测试数据】等功能。这个评测机极大地帮助了我完成作业任务。尤其是模拟运行界面,对于一份输入输出,借助模拟界面可以非常清晰直观地看出来性能的差距和优化点,这也是我优化策略的重要启发工具。同时,互测阶段也帮助我hack到了许多同学的bug点。
我在整个过程中使用过的模型主要有Gemini 3.0、3.1 pro、ChatGPT 5.3 Codex、ChatGPT 5.4等,包括AI studio、copilot以及这些模型官网的。整体用下来感觉AI studio > copilot > 官网。Gemini和ChatGPT也是各有优劣。
最后额外补充一些,也算吐一下黑泥,倾倒一下我最近对AI的感受。里面也有从网上总结的一些观点。如果吵到了你的眼睛,抱歉。
AI发展实在是太快了,它已经渗透到了社会的方方面面,我想至少正在看这篇博客的人或多或少都早有这样的感受。我不知道这是好是坏,亦不知道我是否有资格评判它。
一方面,作为高效的工具,AI能够显著提升我们的工作效率,这是毫无疑问的。但另一方面,我感觉好像有什么东西正在流失。
我在使用AI,我用AI帮我完成任务。但我被AI使用了这么久,自己什么都没学到,像是把AI作为自己与现实世界交互的工具,在电脑前坐了一天。不可否认它们真的很方便,极大地提高了一切工作流的效率。但自从我逐渐依赖这些工具以来,我很少能感受到AI诞生前(或者说我依赖AI之前)手搓一段复杂代码、亲自解开复杂问题、完成一项艰巨的任务后那种发自内心的成就感了,也越来越不想动脑深入思考了。
诚然可能是我的使用方式有问题,我应该拿AI来作为自己学习的工具而不是让它来解决问题。但AI就像一本一应俱全的教辅书,上面有知识点、解题思路、解题大招,更有每道题的答案。现在你是正在写作业的学生,你有这样一本教辅书。你在写作业的时候,是会选择把前面的知识点、解题思路、解题大招给吃透再去自己写作业,还是直接翻到答案直接照抄?
如果是中学时代的我,我一定会选择前者,因为我最后要高考,高考时没有这本教辅书,而且我到了大学我还要在我中学知识的基础上学习。但现在呢?我现在学习的一切东西将来都不会受到考核(除去可能的考研、笔试、面试等等环节),就算是到了工作岗位上,你反而会获得比现在的教辅书更好用的教辅书,你随时随地可以使用它。那既然我所做的一切都是在完成任务,我为什么不选择效率更高的后者呢?
现在的AI还不会在马路牙子上开车,那一年后呢?两年后呢?十年后呢?全世界最聪明的大脑合力走在AI的最前沿,以极快的速度一次又一次地把AI的顶点不断抬高。现在的AI还需要人时刻监督,未来呢?没有人知道未来会变成什么样子。
所以,从实用主义的角度来说,人在AI与人的较量中毫无疑问输得体无完肤。AI比人类强这么多,我们又何必去学AI早就会了的知识呢?社会快马加鞭地拥抱AI,所有人都在说AI就是下一个风口,学会AI就是赢在未来。AI消解了很多价值与意义。那“我”的价值在哪里?“人”的价值在哪里?
我目前还没有找到答案,不知道我们究竟该用什么样的眼光和姿态来看待AI。或许我的这种焦虑是源于我有时会把“自己的价值”和“能做的工作等外在的东西”绑在一起:没有外在,内在也不用发展了。但很多时候,人的内在发展的本身就很有意义,很让人开心。知识经过了我的脑子,但是没有做出什么成果;和AI帮我做了很多事情,但知识平滑地流过我平滑的大脑,不留痕迹。我会认为前者更有意义、更能让我意识到我是个“人”,尽管从外部评价而言,前者是低效且无意义的。在外部评价之外,我依然愿意去自驱地做一些事情,我认为这些事情本身就是“我”的意义。
但话又说回来,或许受LLM底层原理的限制,现在的AI无法诞生出真正的“创造”的智慧,它现在更多地只是把人类开源造好的轮子高效地拼凑起来从而解决问题,实际上它什么都不曾真正“会”过,本质还是概率与统计的结果。所以依我的拙见,“人”的从0到1的创造力是AI无法替代的。
U2相比U1来说,第一次真正接触到了多线程的开发任务,整体几周做下来感觉还是跟U1以及以前写过的代码有挺大区别的。
多线程的开发,我们不仅需要考虑以前那些会出现的细节bug,还要重点关注线程安全,注意锁的设置、同步互斥的安排、高并发的保护、防止出现死锁等等...总的来说,借助电梯这一单元,我对多线程开发建立起了基础认知,也有了实践基础,设计优化策略的过程也是比较有意思的经历。
但说实话,感觉刚开始刚起步的时候有些难以上手,感觉理论没太学明白,具体的代码也不太写得出来。可能是我的问题吧,但是我还是感觉从单线程到多线程的跳跃有些大,要是能多一些细致的引导会更好。
大概就是这些,不过我感觉课程组已经把U2任务设置得很合理了!
另外,我历次作业对应的评测机都有保留下来,我正在着手开发一套更加好用、完整的独属于OO课程的评测机(或者可能是评测网站)。届时我也会将我的评测机在github开源。