2026-OO-Unit2-电梯调度

朱晓天-24373340 2026-04-23 21:32:00

Unit2 电梯调度

同步块的设置和锁的选择

  • 第一次迭代过程中,我将所有同步块都设在了waitQueue这一方法中,因为对于总队列totalQueue,该资源同时被分派线程和输入线程使用,对于每个电梯线程的等待队列,又同时被分派线程和电梯线程使用。并使用synchronized关键字,将waitQueue方法设为同步方法,从而保证同一时间只有一个线程可以访问该方法。将其他访问线程阻塞。

  • 第二次迭代过程中,仍然将大部分同步字段加在waitQueue方法中。不同的是,由于增加了维修过程以及电梯分发逻辑,我们不得不在dispatch方法中也增加一个totalQueue的锁。原因是,当所有电梯线程同时进入维修的时候,为了防止轮询,我们需要再dispatch中加一个totalQueue的wait方法。而于此同时,我们需要再电梯线程完成维修的时候,拿到totalQueue的锁并唤醒dispatch。

  • 第三次迭代过程的双轿厢电梯,则必须增加有关竞争楼层F2的锁,当一个轿厢申请前往F2的时候,必须获得这个锁,当轿厢离开F2的时候,才能释放这个锁。否则,另一个轿厢无法获得F2的锁,从而无法前往F2。同时,还必须保证在双轿厢过程中,某个电梯不能再F2处进入等待状态防止另一轿厢一直无法进入F2.

调度器设计

本设计的调度器采用的是打分机制和动态兜底机制。

首先检测电梯的等待队列人数,每多一个人分数+100,再检测电梯加入新顾客后是否超重,超重+100,再检测是否顺路,顺路-50,再检测电梯是否静止,静止-20,并针对每个电梯,当下最多等待乘客不多于16个人,以免其他电梯维修时将人数全部转移到该电梯,导致其他电梯无法运行。由于电量和时间限制难以权衡,我仅做了适配时间的分派逻辑。

bug分析

  • 第一次迭代:太空电梯:由于Strategy中当电梯出于静止状态的时候,我将电梯的上人逻辑写在了waitQueue中的一个方法里,使用一个while循坏来完成解决每次上的人前往的方向不同的问题,但是由于忽略的线程的时间线性问题,导致direction没有在上人以后及时更新,电梯静止的时候,电梯里同时又向上的人和向下的人,又因为本电梯采用look算法实现,当电梯里还有人没有到达终点时,电梯会继续向上行进,导致RTLE。

  • 第二次迭代:分派超时:当5个电梯线程全部维修的时候,由于没有做16人截断逻辑,导致所有人都被分派给剩余的一个电梯最终RTLE

  • 第三次迭代:F2等待: 为了防止电梯在F2让位的时候刚好recycle结束导致错误的receive要求,我让电梯在出于recycle状态时不能让位,会导致当双轿厢在同时前往F2的时候如果主轿厢先拿到锁,副轿厢会因为主轿厢在F2进入wait状态而recycle超时导致WA

线程安全与层次化设计

线程安全

  • 原子快照与TOCTOU防御: 在电梯开关门(openAndClose)接客的过程中,我深刻体会到了并发编程中“检查与执行时间差(TOCTOU)”的致命危险。起初我把检查人数和上人逻辑分开,结果在中间 sleep(400) 的时间缝隙里,极易被调度器塞入新请求导致容量溢出或丢失唤醒。最终,我将“获取初始排队人数、乘客上车、获取结束排队人数”这三步打包在一个 synchronized(waitQueue) 同步块中,形成绝对的“原子快照”,彻底冻结了这部分操作的时间缝隙。
  • 边缘触发与精准唤醒: 为了防止大堂经理(调度器)因为频繁的 notifyAll() 陷入“惊群效应”导致 CPU 空转,我在唤醒机制上做了极致的抠细节。利用状态的非连续跃迁——只有当电梯的等待队列从“满载(>=16)”跌落回“有空位(<16)”的那一瞬间,电梯才会去获取 totalQueue 的锁并唤醒调度器。这种按需唤醒的“边缘触发”逻辑,极大降低了无用的线程切换。
  • 锁的粒度与死锁规避: 坚持“最小临界区”原则。所有的耗时操作(如 TimableOutput 打印和 sleep 移动)都坚决踢出同步块。在处理双轿厢 F2 防撞时,我使用专门的 ShaftCoordinator 作为井道互斥锁,并在 move() 中配合局部变量来记录持锁状态,彻底杜绝了因电梯被 Recycle 瞬间全局状态突变而导致的锁泄露死锁。

层次化设计

  • 清晰的职责边界划分: 整个系统被切分成了互不越界的几个独立层级:InputThread 充当无情的输入收集器;DispatcherThread 作为大堂经理只负责算分和发牌;ElevatorThread 是纯粹的物理执行机构,只管执行动作序列;而 WaitQueue 则作为共享信箱,把所有复杂的 waitnotify 同步细节封装在内部,对外部调用者透明。
  • 无状态的策略大脑(Strategy): 这是我在架构重构中最满意的一环。我将原本耦合在电梯内部的 LOOK 寻路算法,完全抽离成了一个独立的 Strategy 类。它被设计成一个无状态的纯函数(Pure Function),不持有任何锁,也不修改任何外部变量,只根据传入的参数(当前楼层、车内乘客、队列情况)计算并返回下一步的 Action(向上、向下、开门、等待)。这种设计让多线程控制逻辑和调度算法逻辑实现了完美的解耦。
  • 单向瀑布式数据流: 系统中乘客的生命周期保持了严格的单向流动:Input -> totalQueue -> Dispatcher -> waitQueue -> Elevator。这种单向流转切断了模块间复杂的双向依赖,不仅把死锁的可能性降到了最低,也让我在第三次迭代中加入“改造-回收-维修”这种复杂的系统级状态机时,拥有了一个极其稳固的底层骨架。

大模型使用心得

  • 模型名称:在本次多线程电梯单元的迭代中,我主要使用了Gemini作为辅助开发与架构审查工具。

  • 任务分工:我并没有把它当成“代码生成器”,而是将我们分工我主要进行架构搭建和代码实现部分,大模型负责进行并发安全审查。我负责阅读指导书、确立核心架构(调度器、策略类、双轿厢状态机)并编写业务主体逻辑;大模型则负责“极限施压”,我将写好的同步块、防撞逻辑喂给它,让它从操作系统线程调度的维度构造恶意并发数据,寻找潜在的死锁和 RTLE 漏洞。

  • 复杂并发任务中的优势

    • 极限时序推演:人脑很难在脑海中模拟多个线程在纳秒级别的时间片切分。大模型能够精准捕捉到“在 sleep(400) 期间大堂经理塞入新请求”这种极其隐蔽的 TOCTOU(检查与执行时间差)和 Lost Wakeup(丢失唤醒)漏洞,这在传统的单步调试中几乎不可能复现。
    • 架构模式的升华:它不仅能帮我找 bug,还能将零散的修补提升为成熟的设计模式。例如,在它的引导下,我将粗暴的 notifyAll 优化成了“边缘触发(当且仅当排队人数从满载跌破阈值时唤醒)”,并将分段的开关门操作重构成了“原子快照”,在保证安全的同时榨干了最后一点性能。
  • 遇到的困难与局限性

    • 上下文碎片化:电梯系统包含十几个类,大模型在长对话中很容易丧失全局视野。例如,为了修复一个局部的双轿厢死锁,它曾提出过一种“过度避让”的策略,虽然防住了死锁,但破坏了 Strategy 中原本高效的 LOOK 算法。这就需要我时刻掌控全局约束,不能盲从。
    • 过度设计(Over-engineering):大模型有时会为了炫技,建议使用 ReentrantLock 或高级的并发条件变量。如果在 OO 课程中不加辨别地引入这些复杂的 JUC 包,不仅会让代码显得臃肿,还难以自证正确性。最终我常常需要把它“拉回来”,用最基础、最优雅的 synchronized 块解决问题。
  • 个人使用感受
    在多线程这种充满不确定性的领域,大模型绝不是替代思考的拐杖,而是放大程序员架构能力的放大镜。如果自己不深刻理解对象监视器和锁的粒度,大模型给出的高深同步代码就只是一堆危险的“黑盒”。只有牢牢掌握了底层的并发原理,才能在与大模型的“技术答辩”中驾驭它,把它变成打造工业级健壮代码的利器。未来的编程,正在从纯粹的“Coding”向“系统设计与 Code Review”大步迈进。

体验与感受

第二单元的任务虽然颇具挑战性,但是我们也能从中体会到很多有关多线程冲突处理的思想以及线程安全相关的问题,也确实提高了我们对于多线程问题的视野。但感觉第二次迭代过程中的任务相对较重,希望合理调整任务难度曲线。感谢助教和老师的辛勤付出,希望在后续的学习中,能够更好的学习面向对象的设计思路以及更加巧妙的编程技巧。

...全文
89 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

304

社区成员

发帖
与我相关
我的任务
社区描述
2026年北航面向对象设计与构造
java 高校
社区管理员
  • 孙琦航
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧