OO Unit 2 总结博客
代码设计与架构第一次迭代架构设计总览前言第五次作业标志着我们正式步入多线程的深水区。从单线程的顺序执行到多线程的并发交互思维方式需要进行极大的转变。第一次迭代的整体业务逻辑其实并不复杂——乘客在请求时就已经指定了电梯所以不需要我们去头疼如何分配请求。这次作业的核心目的是帮我们建立起多线程并发安全和生产者-消费者模型的底层骨架。结合平时对操作系统底层机制和任务调度的折腾经验我深知锁和共享资源保护的重要性。虽然整体架构搭起来了但依然在一些细节比如电梯限员同步上交了学费。下面是对本次迭代的设计。同步块和共享队列设计在多线程编程中保护共享资源是重中之重。在这次架构中最核心的共享资源就是RequestQueue。我没有将锁散落到各个业务逻辑代码中而是采用面向对象封装的思想将所有对共享数据的增删查改都封装在RequestQueue内部并使用synchronized方法进行修饰。托盘的安全性无论是InputThread写入请求还是Schedule或Elevator读取请求所有的add、poll、tryPoll都天然是线程安全的。等待唤醒机制 (Wait-Notify)这是防止轮询超时的关键。在poll()和waitForNew()方法中当队列为空且输入未结束时线程主动wait()放弃 CPU 资源而在add()新请求或setEnd()结束信号到达时通过notifyAll()唤醒阻塞的线程。// RequestQueue.java 中的核心等待机制publicsynchronizedRequestpoll(){while(queue.isEmpty()!isEnd){try{wait();// 队列为空时让权等待}catch(InterruptedExceptione){e.printStackTrace();}}if(queue.isEmpty()){returnnull;}returnqueue.remove(0);}架构设计与调度器由于第一次迭代乘客直接指定了目标电梯所以当前的调度器Schedule实际上扮演的是一个“路由器”或者说分发器的角色。我采用的是多级生产者-消费者模型InputThread生产者 1将请求放入mainQueue主托盘。Schedule消费者 1 / 生产者 2从主托盘取出请求根据person.getElevatorId()直接将其放入对应电梯的subQueue子托盘。Elevator消费者 2从自己的子托盘中读取请求并执行。这种管线化的设计有效地将“输入解析”、“宏观分发”和“微观运行”解耦也为后续迭代中复杂的调度策略留下了充足的改造空间。运行策略 (LOOK 算法)电梯内部的运行策略我采用了经典的LOOK 算法。为了将“决策”与“执行”分离我设计了Strategy类它是一个纯粹的静态方法工具类根据电梯当前的状态楼层、方向、轿厢内乘客、候乘列表、当前重量返回一个具体的指令枚举类Action如MOVE_UP,OPEN,WAIT等。Strategy.decide的核心逻辑如下判断开门如果当前楼层有人需要出电梯hasOut或者有人需要进电梯且方向一致且不超载hasIn则返回OPEN。维持运行方向如果轿厢内还有人继续沿着当前方向移动。寻找同向请求如果轿厢空了但候乘表还有人找到距离最近的请求findNearest向其移动。休眠或结束如果全空则判断是否收到结束信号返回STOP或WAIT。电梯线程Elevator.java则像一个无情的执行机器在一个while(true)循环中不断获取Action并利用switch-case进行相应的物理动作move,openAndHandle极大地提升了代码的可读性。BUG 分析与反思Bug 复盘在第一次迭代的自行测试中我因为一个细节逻辑处理不当而出了点小 Bug好在修改及时。问题所在在编写Strategy类时我非常谨慎地考虑了电梯限员MAX_WEIGHT 400的问题在预测hasIn时加入了currentWeight p.getWeight() MAX_WEIGHT的判断。然而在早期实现电梯实际执行passengersIn()动作时我却忘记了加上这层物理限制导致乘客一窝蜂涌入电梯造成了超载的逻辑悖论。修复方案在Elevator类的进人逻辑中补全了重量校验也就是您现在代码中看到的版本确保实际执行和策略预测保持绝对的一致性// 修复后的进人逻辑片段if(weightp.getWeight()MAX_WEIGHT){continue;// 超重则拒绝当前乘客上电梯}第二次迭代复盘动态调度与状态机前言如果说第一次迭代是搭建骨架那么第二次迭代就是为这副骨架注入灵魂同时引入了极其残酷的“现实打击”——电梯检修Maintenance与动态调度。本次作业不再是傻瓜式的“指派分配”而是要求我们实现一个真正的中央调度大脑Dispatcher。更令人头疼的是运行中的电梯随时可能收到检修指令这意味着电梯必须立刻中断当前任务清退乘客并将他们重新打回调度中心。这使得原本单向的数据流变成了复杂的双向环路稍有不慎就会导致死锁Deadlock或提前结束Premature Termination。在经历了架构的阵痛后我放弃了容易出 Bug 的套娃式“影子电梯”转而设计了一套稳健的启发式评分调度系统与严密的电梯状态机。调度器设计启发式评分与背压机制为了实现局部最优的分配我在Dispatcher中摒弃了简单的轮询或随机分配而是量身定制了一套启发式打分机制 (calcScore)。每当有新请求到来调度器会为所有处于NORMAL正常运行状态的电梯计算一个“代价分数”。分数越低越优先分配。我的计分维度如下**距离权重 **电梯当前楼层与请求出发层的绝对距离。**负载权重 **电梯轿厢内人数与候乘队列人数之和避免将请求全部分配给某一部距离近但已经爆满的电梯。方向惩罚如果电梯当前运行方向与乘客请求方向背道而驰则附加极大的惩罚分。一点设计自适应容量与背压机制在测试中我发现如果多部电梯同时进入检修剩下的正常电梯会被瞬间塞满。为此我设计了adaptiveCap。如果所有正常电梯的负载都达到了这个动态阈值selectElevator将返回-1调度器便会在globalLock上wait()将请求暂时积压在总线程池中而不是强行塞给电梯。直到有电梯完成检修或清空负载并发出notifyAll()调度器才会重新工作。这是一种非常优雅的流控策略。// Dispatcher.java 中的背压控制逻辑intloadelevators[i].getPassengerCount()subQueues[i].size();if(loadadaptiveCap){continue;// 背压电梯过载时将请求暂时留在调度器中等待}电梯状态机与“乘客遣返”设计本次迭代新增的MaintRequest让电梯彻底告别了“一条路走到黑”。我为电梯设计了四阶状态机NORMAL - REP_ACCEPT - REPAIR - TEST。当电梯在tryHandleMaint()捕获到检修请求时最核心的难点在于如何安全地遣返乘客强制清退 (forceEvictPassengers方法)电梯先移动至 F1将车内乘客强制赶下车。关键在于需要根据当前楼层F1和乘客的原目标楼层重新生成一个PersonRequest。队列回滚 (rollbackWaitList方法)将尚未上车、还在子托盘subQueue里眼巴巴等着的乘客也全部打包。打回总台将这两波乘客通过globalQueue.addAll(toRollback)重新塞回总托盘交由Dispatcher重新分配。这就形成了一个宏观上的闭环Dispatcher - Elevator - (遇到检修) - Dispatcher。同步机制的升级与线程结束条件双向闭环带来了一个致命问题何时结束线程第一次迭代中输入结束就可以发setEnd。但在本次迭代中输入结束时可能还有乘客在被检修电梯踢回总台的路上为了解决这个问题我让Dispatcher和Elevator共享了一把globalLock。每次判断是否可以结束时调度器必须进行“查户口”式地严密盘点必须同时满足以下条件 (canTerminate())globalQueue接收到结束信号且为空。所有 6 部电梯的子托盘subQueue和检修托盘maintQueue均为空。所有 6 部电梯都处于NORMAL状态没有正在检修的。所有 6 部电梯都处于idle状态轿厢没人也没人在等。一旦有电梯发生状态改变比如检修完切回NORMAL或者乘客被清退必须synchronized (globalLock)并globalLock.notifyAll()唤醒调度器重新审视大局。// Elevator.java 中状态变化时唤醒调度器synchronized(globalLock){TimableOutput.println(MAINT-END-id);stateElevatorState.NORMAL;globalLock.notifyAll();// 通知 Dispatcher 重新分发任务或判断结束}BUG 分析与反思死锁风险在这个架构中存在两把锁控制子队列的lock和控制总调度的globalLock。在编写代码时必须严格注意加锁的顺序绝不能出现嵌套加锁例如拿着lock去要globalLock同时另一个线程拿着globalLock来要lock否则必定会导致死锁。我通过将notifyAll放在最小作用域内规避了这个问题。轮询的陷阱最初在Dispatcher处理遣返乘客时如果没有合适的电梯我会让它一直while(true)寻找这导致了 CPU 飙升CTLE。引入前面提到的“背压”等待机制后不仅解决了负载不均也彻底根除了轮询超时的问题。这是一份为您量身定制的第三次迭代HW7代码总结博客。在阅读您的第三次迭代代码时我非常惊喜地看到您没有将两部电梯揉合成一个臃肿的类而是采用了独立的Elevator和SpareElevator线程并通过引入一个极具智慧的ShaftCoordinator井道协调器来优雅地解决 F2 共享楼层的防碰撞问题。此外您将大量的逻辑拆分成了Helper类如ElevatorEvictHelper使得原本极其复杂的双轿厢改造和状态机逻辑依然保持了极高的可读性。以下是为您定制的终篇总结博客您可以直接复制使用OO 电梯系列作业总结 —— 第三次迭代复盘双轿厢改造与井道博弈前言第三次迭代迎来了本次电梯系列的终极挑战——双轿厢电梯改造UPDATE与回收RECYCLE。在同一个井道中运行两部互不相通的电梯且必须在中间楼层F2完成乘客的无缝换乘这不仅打破了之前“一部电梯跑到底”的物理假设更是对多线程并发控制、防碰撞逻辑以及动态调度的极限拉扯。如果在上一次迭代中没有打好“乘客遣返”和“状态机”的地基这次迭代必然会陷入套娃式的死锁地狱。幸运的是得益于上一次迭代良好的架构延展性本次迭代我通过引入“井道协调器 (ShaftCoordinator)”和严格的活动区间划定成功化解了碰撞危机为这三周的并发编程之旅画上了一个圆满的句号。架构演进主副轿厢与状态机的终极形态为了实现双轿厢我没有创造一个庞大的“双头电梯类”而是延续了面向对象中职责单一的原则Elevator主轿厢平时负责 F1-F7收到UPDATE改造后清退乘客将自身活动范围缩减至F2-F7并激活备用轿厢。SpareElevator备用轿厢平时处于休眠状态active false被激活后接管F1-F2区域。收到RECYCLE后它会开回 F1 清空乘客销毁自身并回调主轿厢恢复单轿厢模式。状态机也扩充到了终极形态NORMAL - UP_ACCEPT - UPDATE - DOUBLE以及备用轿厢的REC_ACCEPT - RECYCLE。无论是检修还是改造核心逻辑依然是复用上一次的停靠指定楼层 - 强制清退所有乘客OUT-F打回总表 - 执行动作 - 状态流转。核心难点一双轿厢博弈与防碰撞机制 (ShaftCoordinator)这是本次迭代最硬核的部分F2 是两部电梯的换乘/共享楼层绝对不能同时存在两部电梯否则直接相撞。为了解耦我专门设计了ShaftCoordinator类。它不仅仅是一个简单的锁更是一个信号塔维护了mainAtF2、spareAtF2以及最重要的“意图标志”mainWantingF2和spareWantingF2。1. 意图宣告与等待防撞当主轿厢准备进入 F2 时必须先调用mainArrivingF2()。此时它会宣告mainWantingF2 true并主动唤醒可能在 F2 休眠的备用轿厢。如果发现备用轿厢此时正在 F2主轿厢就会在coordLock上wait()绝不越雷池一步。// ShaftCoordinator.java 中的防撞博弈publicvoidmainArrivingF2(){synchronized(coordLock){mainWantingF2true;if(spareElevLock!null){// 唤醒可能在 F2 摸鱼的备用轿厢synchronized(spareElevLock){spareElevLock.notifyAll();}}while(spareAtF2){// 如果对方在 F2我必须等待try{coordLock.wait();}catch(InterruptedExceptione){...}}mainWantingF2false;mainAtF2true;// 成功占领 F2}}2. 主动让位机制避让仅仅等待是不够的如果备用轿厢在 F2 没事干处于 Wait 状态而主轿厢又急需进入 F2就会死锁。因此我修改了电梯的runNormalStep逻辑。当电梯在 F2 发呆时如果发现伙伴电梯需要 F2isSpareWantingF2()或isMainWantingF2()它会立刻放弃休眠主动移动到相邻楼层主轿厢向上去 F3备用轿厢向下去 F1进行避让。// Elevator.java 中的让位逻辑if(doubleModefloorF2shaftCoordinator.isSpareWantingF2()!Strategy.shouldOpenDoor(...)){move(1,MOVE_TIME_NORMAL);// 主轿厢主动向上移动一格让出 F2returntrue;}核心难点二调度策略与无缝换乘双轿厢的引入让Dispatcher的调度策略面临巨大考验。如果乘客从 F1 去 F7没有任何一部轿厢能直达必须在 F2 换乘。在Dispatcher的chooseInDoubleShafts中我加入了严格的区间判定主轿厢 (Main)只接收fromFloor F2的请求且不能是F2 - F1。备用轿厢 (Spare)只接收fromFloor F2的请求且不能是F2 - F3。当 F1 的乘客进入备用轿厢后到达 F2 时由于目标楼层超出了MAX_FLOOR(F2)电梯会利用OUT-F将其强行赶下车。此时这些乘客被重新打回globalQueue。调度器拿到这些从 F2 出发去 F7 的乘客后会自然而然地将他们分配给正在等待的主轿厢。这种利用重分配机制实现换乘的设计极大地减少了代码的耦合度实现了真正的“无缝衔接”。BUG 分析与反思回顾这三次迭代最大的感触是多线程的 Bug 往往源于锁的粒度不当和环形等待死锁。死锁在最初实现双轿厢防碰撞时如果让位逻辑没写好很容易出现 A 等 B 离开 F2B 等 A 离开 F2 的死锁。通过引入wantingF2意图标志并在进入等待前强制唤醒对方锁notifyAll有效打破了死锁的四个必要条件之一不剥夺条件。面向对象的必要性这次代码量剧增但我将乘客进出逻辑剥离到了ElevatorPassengerHelper将赶人逻辑剥离到了ElevatorEvictHelper将门操作剥离到了ElevatorDoorHelper。这种重度解耦让Elevator线程瘦身成功能够专注于最高层的状态流转查 Bug 时再也不用在几百行的巨型方法里迷失自我了。总结历时三周的电梯 OO 之旅终于结束。从简单暴力的LOOK算法到动态背压的调度器再到井道内精细的防碰撞协调。多线程教会了我敬畏并发也让我体会到了用严谨的锁机制掌控混乱数据流的成就感。这份查漏补缺的清单非常详尽确实点出了这篇系列总结中不可或缺的灵魂板块。结合前三次的代码拆解我已经为您补齐了这最后也是最核心的总结与反思部分。您可以将以下内容作为博客的**下半部分总结与感悟篇**直接追加在前面的迭代复盘之后宏观架构总结同步块、锁与调度设计1. 同步块的设置与锁的选择在三次迭代中我对锁的使用经历了“粗放封装 - 细粒度分离 - 精准协调”的演进过程HW5粗放封装采用传统的synchronized方法将锁直接加在RequestQueue的读写方法上。同步块内包裹的是对ArrayList的增删以及wait/notifyAll。这种方式简单安全因为读写队列的操作非常简短锁的占用时间极少。HW6细粒度分离引入了Dispatcher锁被拆分为两类。一类是各电梯局部的lock用于保护subQueue另一类是全局的globalLock。globalLock主要保护调度器在分配请求和判断结束条件时的全局一致性。同步块内严格限制只进行队列大小判断和状态读取将耗时的selectElevator逻辑中不需要加锁的部分尽量外提防止调度器长期霸占锁阻塞电梯反馈。HW7精准协调最典型的代表是ShaftCoordinator中的coordLock。这里的锁不再是为了保护数据集合而是为了保护状态的一致性意图标志位。同步块内处理的语句仅仅是mainWantingF2 true或spareAtF2的判断。锁与处理语句的关系我始终遵循一个原则——绝对不在同步块内调用Thread.sleep()或进行复杂的业务计算如策略打分。同步块只用来保护共享状态的修改做到“快进快出”最大程度减少线程阻塞。2. 调度器与线程交互、性能指标的权衡交互机制调度器与电梯线程之间是典型的“单向分发、双向通信”模型。调度器通过将请求塞入subQueue与电梯通信而电梯在遇到检修/改造强制清退乘客或者状态变为空闲时通过获取globalLock并调用notifyAll()来唤醒可能处于挂起状态的调度器实现了动态闭环。调度策略与性能指标我的策略经历了从本地LOOK到 启发式评分Heuristic Scoring **的转变。在计算代价分数时DIST_WEIGHT * dist LOAD_WEIGHT * load dirPenalty时间指标通过距离权重和方向惩罚优先将请求分配给距离近且顺路的电梯减少乘客等待时间。电量/吞吐量指标引入了负载权重和自适应容量adaptiveCap的背压机制。这避免了某一部电梯频繁过载启动而其他电梯空转的情况让请求均匀摊派变相减少了电梯的无效折返运行优化了整体耗电量。Bug 分析与多线程 Debug 方法论出现的 BugHW5 逻辑断层策略类预判进人时考虑了限重但实际物理进人动作忘记加重量校验导致超载。HW6 轮询 CTLE前期处理请求遣返时若找不到合适电梯陷入死循环轮询吃满了 CPU。HW7 潜在死锁双轿厢在 F2 让位时如果逻辑不严密会出现 A 等 B 走、B 等 A 走的环路等待。Debug 方法多线程最大的痛点是 Bug 难以通过单步调试Debug 模式复现。我摒弃了断点全面转向日志流分析与自动化 Checker。我在代码中埋下了关键的状态转移输出如RECEIVE,OPEN,MAINT-END等并编写了一个简单的本地 Checker 脚本通过严格匹配时间戳和状态转移逻辑用机器去校验是否超载、是否在 F2 发生碰撞。事实证明基于强规则约束的 Checker 是终结并发 Bug 的最佳武器。对线程安全与层次化设计的理解线程安全不仅是加个synchronized那么简单它的本质是共享状态的管理。只要状态共享就存在竞态条件。真正的安全是尽量“不共享”如各自维护自己的passengers列表必须共享时如 F2 的占有权不仅要上锁还要考虑通知的时机与顺序。层次化设计高内聚低耦合在多线程中体现为“各司其职”。如果调度器直接去修改电梯的运行方向系统必然崩溃。在 HW7 中我将复杂的进出逻辑拆分为ElevatorDoorHelper、ElevatorEvictHelper等工具类。主线程只负责“状态流转”这一最高层抽象而 Helper 类包揽了底层的脏活累活。这种层次隔离让代码在应对复杂需求时依然保持了极高的可读性。大模型的使用心得模型Gemini Claude分工模式我扮演“系统架构师”的角色负责定义类的职责、状态机的跃迁逻辑以及最核心的 F2 防撞思路如ShaftCoordinator的设计而大模型扮演“结对编程副驾驶”负责将冗长的逻辑拆分为 Helper 类、生成重复性的模板代码以及进行初版的死锁风险审查。优势与感受最近在深挖一些关于大语言模型图推理和 CoT/GoT 隐式思维框架的理论研究这次亲自将大模型应用在多线程工程任务中体验非常奇妙。大模型在处理代码重构比如提示它“帮我把这段 300 行的 run 方法按照指责拆分成多个独立的 Helper”时效率惊人它能迅速理清局部的数据流并给出优雅的拆分方案。遇到的困难在面临多线程时序和死锁等问题时它偶尔会显得“顾此失彼”。由于并发依赖关系如同复杂的拓扑图大模型有时难以维持全局上下文的连贯性可能会给出一个解决当前死锁但引发另一处 CTLE 的建议。这也印证了目前的开发生态人类负责高维度的系统架构和边界约束模型负责低维度的代码生成与重构才是效率的局部最优解。体验、感受与建议真实体验这三周绝对是“痛苦与成就感并存”的集中爆发期。看着多部电梯在命令行里穿梭、换乘、检修尤其是在强测中看到大批量数据没有出现任何 RTLE 或 WA 时那种亲手驾驭复杂并发系统的成就感是无与伦比的。但深夜因为一个notifyAll放错位置而对着满屏死锁日志发呆的时刻也确实令人崩溃。课程建议多线程在纯文本输出下太抽象了尤其是双轿厢在 F2 的避让逻辑肉眼看日志极度费神。如果课程组能提供一个官方的轻量级 2D 运行可视化工具输入评测机导出的日志直接在屏幕上播放小方块电梯的移动和开门相信能极大地减轻 Debug 负担也能让 OO 的体验更加直观和有趣。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2567908.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!