309
社区成员
发帖
与我相关
我的任务
分享
三次作业自始至终采用一套统一的架构:生产者-消费者模式(OOLens 公众号推送的那种类似三明治的架构,3 个层次,2 个中介容器),核心是 Queue-Thread 的组合。InputThread 作为生产者将请求写入 ScheduleQueue,DispatchThread 从 ScheduleQueue 取出请求分派给各 ProcessQueue,各 ElevatorThread 作为消费者从自己的 ProcessQueue 中取任务执行。这一架构在三次作业中没有发生结构性变化,只是随着功能增加而扩展。
最终算上 Main 共 9 个类,类的架构如下:

所有 Queue 类(ScheduleQueue、ProcessQueue)的方法均使用 synchronized 修饰,保证对内部集合的读写操作原子性。具体来说:
锁与同步块中语句的关系遵循一个原则:同步块内只做最必要的操作,不包含耗时的 sleep 或 IO。例如 poll 方法只在 synchronized 块内执行 wait 和队列操作,移动电梯、输出日志等耗时操作都在锁外(ElevatorThread 里)进行,最大限度减少锁的持有时间,降低线程间的争用。
第一次作业由乘客自行指定电梯,ScheduleQueue 按电梯 id 直接分发,ProcessQueue 逻辑简单,只需基本的 synchronized 保护。第二次作业引入 DispatchThread 进行调度,并新增 MaintRequest,需要 maintLock 协调检修期间的线程等待。第三次作业新增 UpdateRequest 和 RecycleRequest,update/recycle 标志位的读写也需要在适当位置通知等待中的 DispatchThread,整体锁结构在第二次作业基础上自然扩展,但没有引入新的锁类型。整体来看,三次作业只是新增了 Request 类型,在多线程安全角度我认为并没有新增迭代任务。
调度器由 DispatchThread 单独承担,作为一个独立线程运行。它从 ScheduleQueue 中循环取出请求,根据请求类型分别处理:
DispatchThread 与其他线程的交互依赖两个机制:一是 ScheduleQueue 的 wait/notifyAll 实现请求的生产-消费同步;二是 maintLock 实现当所有电梯均不可用时 DispatchThread 的阻塞等待,以及电梯恢复可用后的唤醒通知。
针对 PersonRequest,采用基于优先级和分数的贪心策略,为每部可用电梯计算一个(优先级,分数)二元组,取最优者:
此策略主要优化时间性能:优先利用顺路的电梯,减少空跑。对于电量指标本次未做专项优化,电量消耗随时间优化间接降低。在双轿厢模式下,通过奇偶井道的静态范围划分(奇数井道主轿厢 F2-F7、备用 B4-F1;偶数井道主轿厢 F3-F7、备用 B4-F2)规避碰撞(这省去了很多时间),并通过换乘(OUT-F 轰出后重新入队)实现跨范围乘客的运送。
第一次作业未出现 bug,逻辑相对简单。
第二次作业出现了一个线程安全问题:people 列表(存储当前电梯内乘客)最初使用普通 ArrayList,在 ElevatorThread 写入和 getElevatorInfo 读取并发时出现数据竞争,导致偶发的 ConcurrentModificationException。修复方式是将其替换为 CopyOnWriteArrayList,读操作无需加锁,写操作(上车/下车)在 ElevatorThread 单线程内进行,问题消除。
第三次作业调试过程中遇到以下问题:
面对多线程程序,主要采用以下方法:
线程安全的核心在于识别共享可变状态,并为其选择合适的保护手段。本次作业中共享可变状态主要有三类:
一个重要教训是:线程安全不只是加锁,还要注意可见性。people 列表的 ConcurrentModificationException 就是忽略了并发读写的结果,替换为 CopyOnWriteArrayList 后通过结构保证线程安全,而非依赖锁。因为此处 people 并不是一个我自定义的线程安全的容器,所以要特殊考虑。
层次化设计体现在职责分离上。InputThread 只负责读入,不做任何判断;DispatchThread 只做调度决策,不执行电梯动作;ElevatorThread 只关注自己的状态机,不关心其他电梯。各层之间通过 Queue 传递数据,通过标志位和锁传递控制信号,耦合度低。
第三次作业新增双轿厢功能时,这一层次化架构体现出良好的扩展性:只需在 ElevatorThread 内新增 UPDT/RCYC 状态,在 DispatchThread 内新增对 UpdateRequest/RecycleRequest 的处理分支,以及在 updt() 内动态 start 备用轿厢线程,不需要修改 InputThread 或 Queue 的接口。处理方法类似于处理 MaintRequest,只需要加两个新状态代表 update/recycle 即可。
本次作业全程使用 Claude,主要用于架构设计讨论、代码 review、bug 分析三个方面。分工上,我负责整体思路、代码实现和测试,Claude 负责在我思路不清晰时给我提示,在我描述问题后给出分析和建议,或在我提供代码后指出潜在问题。
在多线程电梯这类复杂任务中,大模型的优势体现在以下几点:
大模型也有明显局限:
使用大模型的感受是「提效但不替代思考」。它更像一个随时可以讨论的搭档,快速过滤明显的错误,给出方向性建议,但最终的设计决策和验证,以及更复杂的 debug 还是需要自己来。对于多线程这种「魔法时机」才能触发 bug 的领域,大模型的作用更多是在设计阶段提前规避风险,而不是在出了 bug 之后神奇地找到根因。
二单元整体难度曲线比较陡峭,第一次作业相对平缓,第二、三次作业引入了大量新的状态和边界条件,是整个单元中最耗时、最难的部分,尤其是第二次作业,因为这是首次出现电梯需要轰出未到终点的乘客的作业。多线程调试的痛点在于 bug 难以稳定复现,往往需要构造非常精确的时序数据才能触发,调试效率较低。
相比逻辑 bug,线程安全问题更难察觉——people 列表的并发问题在大多数测试中都不会触发,只有在高并发场景下才偶发,这让人容易对代码的正确性产生虚假的自信。
指导书篇幅偏长,且包含了大量在前两次作业中已经出现过的通用性、显然性的约束(如「移动时不许上人」、「移动时必须关门」、「有需求或有人才能动」等)。这些约束在第一次作业就已内化,反复出现在后续指导书中反而会造成信息噪声,让人在阅读时难以快速定位真正新增的约束点。
建议将不变的通用约束单独整理成一份参考文档,各次作业的指导书只描述新增和变更的内容,这样既能缩短阅读时间,也能让同学更清晰地把握每次作业的增量变化,避免指导书越来越长、越来越难读的问题。