304
社区成员
发帖
与我相关
我的任务
分享如果说第一单元的表达式解析是对面向对象思维的初步建立,那么第二单元的多线程电梯调度,则是带我们真正走进了并发编程的“深水区”。从最开始的单体电梯、生产者-消费者模式,到动态重置,再到最后让人心力交瘁的“双轿厢”互斥换乘,多线程的不确定性让 Debug 过程充满了玄学色彩。在这里,对这三次作业的架构演进与踩坑历程进行一次系统性的复盘。
本单元的核心架构始终围绕生产者-消费者模型展开。
输入线程 (Producer):负责接收外部请求,放入中央调度队列。
调度器线程 (Dispatcher):负责将请求按照特定策略分配给对应电梯的局部队列。
电梯线程 (Consumer):负责消化局部队列中的请求,执行上下行和开关门。
本次作业是多线程的初体验。为了保证线程安全,我采用了最基础的 synchronized 关键字配合 wait()/notifyAll() 方法来实现共享队列(WaitQueue)。整体架构比较清晰,输入线程作为生产者,电梯作为消费者。调度器采用自由竞争或者简单的均分策略。重构体验在于需要把单线程的顺序执行思维转变为并发思维,核心是明确哪些对象是共享的,并对共享对象的方法加锁。
第六次作业新增了电梯的动态重置(Reset)指令。电梯在运行中途随时可能收到重置请求,必须平滑地停靠、释放乘客并改变自身参数。为此,我引入了状态机模式,将电梯状态细分为 RUNNING、RESETTING、STOPPED 等。当收到重置信号时,电梯线程优先处理,将车厢内及等待队列中的乘客全部“吐出”退回到主请求队列,交由调度器重新分配。这次作业极大考验了多线程下的状态一致性维护。
这是本单元最复杂的一次作业,重点在于实现“双轿厢(Double-carcass)”电梯以及在特定楼层(F2层)的换乘。
双轿厢互斥锁:两个同轨道的轿厢绝对不能同时出现在换乘层(F2)。我在这里设计了一个针对 F2 层的共享锁对象。进入 F2 前必须尝试获取该锁,离开后释放,严格避免了“撞车”事故。
请求拆分与换乘调度:对于跨越 F2 的请求,需要在调度器或流水线中将其拆分为“起点到 F2”和“F2 到终点”两段。乘客在 F2 走出第一个轿厢后,其后半段请求被重新投入共享队列或直接交给另一个轿厢。逻辑的耦合度在此处急剧上升,但得益于前两次作业良好的封装,整体架构并未发生大规模雪崩。
通过度量工具分析本单元的代码结构,可以看出明显的变化趋势:
类属性与方法规模:Elevator 类和 Dispatcher 类的代码行数和属性数量在第七次作业达到了顶峰。由于加入了双轿厢逻辑,电梯类中包含了更多关于运行区间、换乘状态的属性。
控制分支复杂度(Cyclomatic Complexity):调度算法(如 Look 算法)和电梯内部的运行逻辑(开门、关门、上下行判断)控制分支较多,尤其是 getAdvice() 这种判断电梯下一步行为的方法,嵌套了较多的 if-else。
高耦合点:共享请求队列(RequestQueue)是系统中耦合度最高的类,所有的线程都在与它交互。因此,该类的线程安全性和方法的原子性是整个系统稳定的基石。
在本地测试和公测中,我主要遇到了以下几类典型的多线程 Bug:
死锁 (Deadlock):在第七次作业双轿厢交互时,由于获取 F2 层互斥锁和获取电梯内部对象锁的顺序不一致,导致了经典的死锁。解决方案是严格规定所有线程获取锁的全局顺序,避免循环等待。
轮询 (CPU Time Limit Exceeded, CTLE):在初期实现时,因为某些条件判断遗漏了 wait(),导致线程在 while 循环中疯狂空转,CPU 满载。通过仔细梳理状态转移图,确保每一个不满足执行条件的循环都会进入阻塞态。
并发修改异常 (ConcurrentModificationException):在遍历请求队列进行分配的同时,如果有其他线程(如 Reset 后的退回操作)修改了队列,就会抛出该异常。最后统一使用了线程安全的读写锁(ReadWriteLock)或并发容器来解决。
多线程的 Bug 往往具有不可复现性,因此在互测环节,纯靠手捏边界数据去 Hack 的效率并不高。我的测试策略分为两步:
自动化黑盒狂轰滥炸:编写 Python 脚本自动生成大量极端测试数据,尤其是高密度的突发请求(Burst Inputs)和密集的 Reset 信号。
针对特定逻辑的白盒狙击:在阅读同房间同学的代码时,我会特别关注他们对双轿厢的处理。比如在面对代码风格严谨的“天枢星”和“开阳星”同学时,我会专门构造针对 F2 换乘层进行密集人员投放的数据,或者在同一时间点疯狂投入换乘请求,以此来探测对方是否完美处理好了双轿厢的互斥锁问题,以及会不会因为锁粒度太大而导致性能严重下降。
本单元的性能分竞争非常激烈,我主要在以下两个方面进行了优化:
Look 算法:放弃了基础的傻瓜调度,采用了类似现实生活电梯的 Look 算法(同向一直走,直到该方向无请求才掉头),极大地减少了无意义的折返跑。
影子电梯(Shadow Elevator)调优:在调度器分配乘客时,为了避免“旱的旱死,涝的涝死”,我尝试引入了影子电梯策略。通过深拷贝当前电梯的状态,模拟计算将新请求分配给各个电梯后的总运行时间,选择时间增量最小的电梯进行分配,实现了局部最优的负载均衡。
在并发编程中,AI 工具成为了我极好的“助教”:
锁机制释疑:在使用 wait/notify 感到吃力时,我向 AI 询问了 ReentrantLock 和 Condition 的具体用法,并成功在复杂场景下替换了原有的锁机制,实现了更细粒度的唤醒。
正则表达式检查:输入解析部分的复杂正则依然需要 AI 帮忙排查漏斗。
死锁分析:将产生死锁的简化版代码逻辑喂给 AI,它能迅速帮我画出锁的获取有向图,精准定位成环的原因。
学习心得: 多线程的 DEBUG 是痛苦的,它彻底打破了第一单元“只要逻辑对,结果就一定对”的线性思维。但当看着控制台上几十个线程有条不紊地工作,双轿厢电梯在 F2 层完美错开、高效运转时,那种成就感也是无与伦比的。面向对象的封装特性在多线程中显得尤为重要,只有把共享资源的访问接口封装得滴水不漏,才能避免外界对状态的随意篡改。
对课程的建议: 建议在第六、第七次作业发布前,可以在理论课或指导书中多提供一些关于“状态机”和“多级流水线模式”在并发场景下的标准设计模式参考。双轿厢的逻辑对于初学者跨度较大,如果有官方的轻量级并发测试框架或可视化检查工具,可能会极大减轻大家在本地捉盲盒 Bug 的痛苦。