OO U2 总结博客

申扬-24371250 2026-04-29 22:28:56

本文约 3000 字,阅读预计需要 10 分钟。

欢迎大家前往 OOpre 2025 与 OO 2026 讨论区阅读我的往期文章。

同步互斥

不手写同步就是最好的同步。

hw5

在 hw5 中我完全没有使用 synchronized 关键字,而是完全依赖消息队列 + BlockingQueue 来实现无轮询 + 无死锁的生产者-消费者流水线。

同时,我使用毒丸实现各线程的优雅停止。在 InputHandler->Dispatcher->Elevator 流水线中,前序线程停止时,会向后序线程的消息队列中投递一个 EndRequest(毒丸),后序线程收到后,会完成剩余的工作,释放可能的资源,随后停止,这样就解决了线程安全停止的问题。

hw5 同样没有使用到 wait()notify()

hw6

由于 hw6 我使用了影子电梯,在获取某个电梯的快照时,需要确保数据的一致性,因此我引入了项目中的第一个锁。这个锁仅仅对电梯的一部分数据的写操作和快照创建的读操作加锁,保证创建快照时电梯不能继续写入,以免向影子电梯传入不一致的数据,并不涉及控制流。

控制流仍然依靠消息队列实现。如维护请求依靠 DispatcherElevatorBlockingQueue 投递 MaintainRequest,并将维护计数加一,Elevator 完成维护后再向 DispatcherBlockingQueue 投递 MaintainDoneRequest,让 Dispatcher 将维护计数减一。这样所有的阻塞都发生在各个线程私有的消息队列上,实现了架构中无轮询死锁。

hw6 中出现了 ElevatorDispatcher 的回传,使得流水线不再是线性依赖关系,单纯的毒丸也不再适用。由于我的逻辑中,只有进入维修的电梯才会向 Dispatcher 回传请求,因此通过上面的维护计数机制,在输入结束后,Dispatcher 等待所有维护请求结束(此时不会再有请求回来)再结束,就实现了安全停止。同时我把维护计数设置为了由 Dispatcher 私有维护而不是 DispatcherElevator 共同维护,避免了潜在的锁竞争。

hw2 仍然没有使用到 wait()notify()

hw7

hw7 要求实现双轿厢电梯不能在 F2 相撞的功能,因此我引入了项目中的第二个锁,一个单独的锁类,并对每个电梯井实例化,以保证两个轿厢只能交替进入 F2。这个锁类的结构也非常简单,无法获取锁时 wait,释放锁时 notifyAll(),这也是项目中唯一一次使用 wait-notify。

其他的控制流继续依赖消息队列,包括电梯升级到双轿厢的 UpdateRequest 和和回收到单轿厢的 RecycleRequest,仍然由 Dispatcher 投递和维护计数,Elevator 返回消息。但由于优化的需要,Dispatcher 不得不使用带延时轮询的设计(在无电梯可用时,Dispatcher 需要暂存请求)。实测没有出现 CPU 超时。

hw7 的线程安全停止只需要加上 Update-Recycle 的计数(仍然只有这时候电梯会回传请求),让 Dispatch 等待输入和两个计数都完成再停止,就解决了线程安全停止问题,总体上是简单延续 hw6 的思路。

调度器设计

我的调度器选择是影子电梯。具体来说,我将往年学长的 blog 作为参考资料输入 AI,得到了现在的成品。

由于感觉写出好的调度器实在太难,我选择直接照抄往年学长的调度思路。hw6 只考虑最小化运行时间,放弃开门关门电量等等因素,hw7 直接把参数改成 运行时间/楼层,让双轿厢电梯能参与比较。遇到的困难主要是调度器过大过重,难以修改和优化。(原因是电梯本身就是一个巨大的状态机,代码复杂度难以优化)

强测出错的数据点太多,所以没有过于关注性能分。

架构上,调度器是一个架构确定的独立的类,从输入线程的消息队列获取解析好的输入请求,再按照给定的策略转发到不同电梯的消息队列。只要通过构造函数注入不同的调度逻辑,就可以实现不同调度策略,也方便比较。然而由于时间因素,我没有实现其他的调度策略(如自由竞争),这就让调度器的架构设计失去了意义。

bug 分析

hw5

hw5 的作业要求非常简单(相比 hw6 和 hw7,或许有点太不平衡了)。我只出现了一个 bug:由于没考虑数组下标从 0 开始,导致访问 6 号电梯时产生越界。

令人震惊的是,中测和强测居然没有任何数据点访问到了 6 号电梯,导致有如此严重 bug 的代码能够进入 A 房。在我看来这是数据组助教的一个重大疏忽。

hw6

hw6 中,收到维护请求的电梯需要把无法处理完成的请求回传给主队列,这就引入两个 bug 点:

  1. Dispatcher 不能过早下班。在输入结束后,Dispatcher 需要确定不再有电梯会回传请求才能停止。
  2. Dispatcher 不能急着投递所有请求。如果电梯不可用或负载过高,应该停止向其派发请求。如果所有电梯都不能接受请求,则应当暂停派发请求。

第一个 bug 在中测截止当晚就被我自己的评测机发现,这大大改变了我的开发思路。从作业优先到评测机优先。

第二个 bug 仍然来自人类手工构造的 corner case 而不是评测机的随机数据,这警示我们不能完全依赖评测机。

hw7

hw7 中,双轿厢电梯需要启动和停止备用轿厢线程,这就产生了新的 bug。轿厢回收时,备用轿厢应该先把剩余的请求回传,再自我结束,我的 AI 搞反了这两者的顺序,造成大量乘客丢失。

这个 bug 和其他三个 bug 一起,在中测截止当天就被我的评测机发现。然而由于当日我恰巧遇到了 rate limit,只能尝试手工排查和修复 bug,导致最终修复也没来得及完成。

测试原则

三次作业中我总结出的测试流程是:

  1. 单元测试:对于特定的功能模块,应该尽可能编写 JUnit 单元测试。如果单元测试较繁琐,也可以让 AI 代为生成,但要注意保持简洁和人类可审阅,以防 AI 出错。
  2. 回测:完成基本功能后,使用评测机载入之前作业的全部公开弱测和中测数据,全部强测数据和互测出错数据进行回测,保证不引入新的 bug。
  3. 压力测试:使用评测机生成随机数据进行测试。
  4. 边界测试:尝试人工构造 corner case 进行测试。(很遗憾,我在这方面不擅长!)

此外,随着作业的推进,我从“先完成功能代码,再开发评测机”逐步转向了“先开发评测机,再编写功能代码”,原因之一是我认为在开发评测机的过程中,我学到了比写作业更多的知识,原因之二是课程组提供的中测强度过低
,不够可靠。

线程和层次化

线程与资源

我觉得 Java 在资源所有权这一方面真的是个糟糕的语言。

尽管 Java 没有明确的所有权机制,而是通过 GC 管理资源,我仍然尽可能保证代码中资源的所有权是明晰的。

具体地说,我的 Elevator 选择了实现 Runnable 接口而不是继承 Thread 类,试图在语义上明确 Elevator 承载的是具体的任务。使用 Elevator 初始化 Thread 对象,Thread 对象才是资源的真正所有者,也是程序员需要监测和考虑其声明周期的对象。

消息队列

在解决线程间通信的问题时,我没有使用 synchronizedwait-notifyinterrupt 等较底层的机制,而是使用 BlockingQueue,将要传递的消息打包成 Request 发送出去。

这种模式的优点有许多,一是实现了生产者和消费者的解耦,双方完全不需要关心对方的状态;二是集中了线程状态的关注点,如果一个线程不在运行也没有停止,那么它一定阻塞在自己的消息队列上,绝无其它可能,这就有效避免了死锁。

然而,使用消息队列也有缺点。一是不够灵活,线程间通信变得复杂,就需要注册更多的 Request 派生类;二是不能保证消息处理的及时性,时间上靠后但是更重要的信息(如维护请求)可能被延迟处理造成超时;三是使我错失了练习基础锁的机会,导致 OS 课上学习困难。

另外,我认为如果使用 PriorityBlockingQueue 替代 BlockingQueue,可以一定程度上解决消息处理的及时性的问题,然而此时 hw7 已过,没办法去实践了。

或许使用 C++ 理解基本的锁更好。C++ 有语义明确的 std::mutex,不是和 Java 一样对象内部隐式携带锁。

策略模式

电梯的维护,双轿厢更新和回收都有大量冗长复杂的逻辑(主要是一堆 sleep 和输出,信息密度低),很容易使 Elevator 类超过 CheckStyle 限制。为此,我去学习了策略模式,将这些特定的行为拆成独立的类,将电梯与电梯行为解耦,实现了文件长度压缩。

很遗憾我到 hw7 才知道策略模式。如果我 hw5 就知道的话,一定会把所有行为都写成单独的类,而不是只有维护,更新和回收是单独的类。

大模型相关

OO 已死……?

—— 沃 · 兹基硕德

我要再次重复这句话:在好的架构设计控制与 TDD 工作流下,AI Agent 的开发效率实际上已经完全超过人类。

本单元我尝试把评测机接入 AI,让 AI 自己编写代码,运行评测机,阅读返回结果,查找 bug,再修改代码直到正确。结果表明这极大扩展了 AI 的能力。

在 hw6 和 hw7,我基本上只是把指导书复制粘贴到 Markdown 格式,要求一个 Agent 根据指导书更新评测机的 Checker 模块(基本上就是状态机模拟所以非常简单),调试通过后,交由另一个 Agent 生成代码,并且让它自己利用评测机检查,这样 AI 就能生成许多可以完成功能要求的代码。我只需要根据任务的不同,把任务分派给不同的模型(我使用的主要是 Github Copilot Edu 中的 GPT-5.3 Codex 和 GPT-5.4 Mini),让他们自行处理即可,很多时候模型甚至就挂在后台,人可以去做其他的事情。

然而,由于 Github Copilot 在本月突然收紧了使用限制,我的 Copilot Edu 套餐无法继续承受 AI Workflow 带来的巨大消耗,许多代码生成到一半后因 rate limit 而暂停,只能由人工补全,这导致最终生成的代码质量不高,存在许多隐性 bug 且难以阅读和修改(没有时间重构,不得不在 AI 已完成的基础上继续开发)。这对我来说是一个巨大的打击,但也告诉我不能完全依赖 AI,毕竟还是存在极端情况 :(

此外,对 AI 的使用方法也很大程度决定了它的能力。AI 不擅长阅读极长的 stdin 和 stdout 记录,因此我让 AI 调整了评测机,使其输出详细的报错信息(某行某乘客重复下客等),并在 prompt 中加入了以下几条:

  • 排查错误时,先根据评测机输出,定位到输入和输出文件。
  • 根据评测机的输出信息,使用命令行工具中在输入输出文件中查找相应的行并阅读,而不是阅读整个文件。
  • 如果不能直接定位错误原因,可以根据输入输出文件的结果,在程序中的有关逻辑段插入 printthrow,向控制台打印调试信息。
  • 执行修改后的代码,将输出重定向到文件,用命令行工具筛选包含调试信息的行,阅读并查找可能的错误点。

这些 prompt 产生的思路是让 AI 模仿我手动调试(阅读评测信息,Ctrl+F 搜索相关行,插入 print 调试信息,模拟执行,阅读调试信息,定位错误并修改),结果表明效果很好。

总而言之,我的感受是:好的外部工具,人类的经验,都可以使 AI 使用更少 token 和更少时间完成更复杂的任务。

心得体会与未来方向

  • 希望课程组延续之前 OOpre 的风格,对 Junit 的使用提出要求(可以不像 OOpre 那么严格)。与其他同学交流的过程中发现有些同学完全不编写任何测试。
  • 感觉在本章的开发中,同样都是 Vibe Coding,我从写作业中几乎无法得到任何知识,但感觉在编写评测机中有许多新的体会。
    • 比起多线程,U2 更应该叫状态机大模拟,任务的复杂度过高,掩盖了同步互斥的中心主题。
    • 评测机编写反而是一个明确,有良好约束而且实现思路清晰的任务,可以让我思考什么样的架构更好,如何利用多线程(我觉得我在这学到的多线程知识比写作业多多了!)加速评测等等。
  • hw5 居然出现了弱测中测强测全部不测 6 号电梯的情况,希望数据组助教予以修复。
  • hw5 相比 hw6 和 hw7,任务过于轻松,显得任务前轻后重。希望之后能把调度器移到 hw5,hw6 实现维修电梯和双轿厢电梯(不带切换),hw7 实现可切换电梯,这样也许能更好地平衡三次作业的任务量。
...全文
39 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

304

社区成员

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

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