Contents

BUAA-OO-Unit2总结

锁与同步块

同步块的设置和锁的选择

  • RequestQueue的synchronized方法:队列中全部方法均使用synchronized修饰,保证核心数据结构的线程安全。
  • Elevator的synchronized方法:receiveRequest startMaintain startUpdate startRecycle四个方法使用synchronized修饰,保证对同一个电梯的状态修改不会出现竞争。
  • shaftLock对象锁:为每个电梯管道创建一个shaftLock作为Object锁,并在双轿厢移动时使用synchronized(shaftLock)包裹,以防止两轿厢碰撞。
  • volatile变量:Elevator中的currentFloor currentWeight direction maintainState runState变量使用volatile修饰,保证每次读取都会从内存获取最新值,保证当前获取到的状态是最新的,同时避免使用重量级synchronized锁。

锁和同步块的关系

保护区域 竞争线程
RequestQueue 队列的增删改查+wait/notify InputTask、ElevatorTask、DispatchTask
Elevator 电梯状态变更 InputTask、DispatchTask
shaftLock 双轿厢防碰撞 同一管道的两个ElevatorTask

调度器

调度器设计

三次作业均采用单调度器设计,即由一个ElevatorTask线程进行乘客的分配。与其他线程交互如下:

        InputTask ---> addRequest()
                   |
                   |
                   |
                   v
               waitQueue
                   |
                   |
                   |
                   v
     elevatorTask ---> getElevatorId()
                   |
                   |
                   |
                   v
              ElevatorTask

调度策略

  • hw5中,由于输入指定了电梯,故Dispatch只负责根据解析结果传入对应电梯。
  • hw6/7中,在Dispatch中设计了代价损失函数,计算分配个每个电梯的损失,选择损失最小的电梯进行分配。为适配性能指标,代价损失函数中计算了方向、距离、重量、队列长度等多种指标。

Bug与Debug

三次作业中,共遇到了如下几个较为严重典型的bug:

  • 时序Bug:打印信息与实际操作的顺序错误导致。如果先进行了实际的操作后打印信息,那么如果线程恰好在这两步中间切换,而新线程恰好根据新的状态进行了操作,那么最后输出的时间间隔甚至输出顺序可能出错。而且这种bug无法稳定复现,所以较难排查。
  • 线程空转:hw7中RequestQueue类中的waitForRequest方法应检查isEnd()方法(包括isEnd字段和count是否为0),但实际只检查了isEnd字段,导致input结束但count > 0时没能正确阻塞,dispatch线程一直在占用CPU,最终导致CPU超时。
  • 队列长度限制:起初没有对电梯等待队列进行限制,这有可能导致分配器将所有请求分配给同一个电梯(特别是在其他电梯全部被ban的情况下),从而导致超时。

Debug方法:

  • 开发带有错误说明的评测机,并根据错误说明定位到错误原因
  • 利用AI辅助Debug

线程安全与层次化

线程安全

经过三次作业,我认为,线程安全最核心的思想就是由于CPU对线程的调度不可预测,我们需要把不能被打断的指令加锁,防止产生不可预期的后果。

  • hw5: 数据加锁。本次作业主要的锁的位置是RequestQueue数据类,这也是最朴素的加锁原因:为了防止线程A在对数据修改到一半时被线程B打断进行其他的访问/修改操作,我们需要对核心数据结构进行上锁,保证线程安全。
  • hw6: 状态加锁。本次作业为电梯增加了不同的状态,因此我们需要对电梯状态的修改方法进行上锁保护,防止状态没有修改完成就被打断,电梯以原状态继续工作等不可预计的后果。
  • hw7: 活锁与死锁。本次作业引入了双轿厢机制,在防碰撞逻辑中可能发生两个轿厢同时在2楼两侧等待对方让路的情况。因此我们为每一个电梯管道引入一个shaftLock锁。shaftLock锁的引入,首先保证每一个电梯需要移动到2楼时,必须先拿到锁,防止出现他刚要移动到2楼被另外一个电梯抢险导致的碰撞。同时在synchronized代码块中加入wait(100)主动阻塞,使得锁有机会被重新竞争,让另外一个轿厢有机会抢到锁,并进行主动避让(Strategy返回)。这样就在避免死锁的情况下解决了碰撞问题。

层次化设计

层次化设计带来了显著的线程安全收益。

  • 数据层:RequestQueue进行了synchronized保护,对外暴露线程安全的数据结构。
  • 逻辑层:Elevator类管理自身状态,Strategy类负责纯逻辑运算,他只接收当前快照并返回结果,无需考虑线程安全问题。
  • 任务层:每一个RUnnable封装一个独立的线程,线程间只通过全局的等待队列通信,互相不暴露内部信息。
  • 组装层:ProjectStarter只负责线程启动,不参与具体逻辑。

大模型的使用心得

  • 中测前:在前两次作业中,只使用大模型整理任务 (Gemini 3 Flash),具体的实现由我自己完成,调度算法等与大模型进行讨论 (Gemini 3.1 Pro),最后由大模型负责Debug (Claude Opus 4.6)。第三次作业,自己只负责提供思路,具体实现交给大模型 (Gemimi 3.1 Pro),并由大模型进行Debug (Codex 5.3)。其中前两次作业顺利通过中测,但是在强测和互测中测出来好几个bug,而第三次作业无法顺利通过中测,只有在结合评测机和课程组评测结果给出的bug信息的情况下,才顺利debug通过中测,但是强测几乎所有数据点都产生了CPU超时的问题,喜提第一次O房了咱就是说
  • 强测及互测后:在前两次作业中,由于能得到Bug数据点以及具体的报错信息,所以大模型定位和修改的非常快,效果也比较好。但是第三次作业中由于CPU超时掐断,我们并没有获取到报错信息,所以Codex没有很好的定位到具体的错误原因。正赶上Deepseek v4发布,想着是一个很好的测试Deepseek v4的机会,便在Claude Code里接入了Deepseek v4,让他进行debug。但是不幸的是,他也没有解决问题。但是一次充的钱还没有用完,再加上突然开了优惠羊毛不薅白不薅,我便给了他更强的提示词(包括可能的错误原因猜测、Codex的失败方案等),他这次很好的解决了问题。当然,我想这也得益于Claude Code的强大Agent能力,让大模型本身的能力有更大的进步吧。(为什么没用Claude呢,因为我没有Claude账号,而是在Antigravity里使用的,但是最近Antigravity经常网络拥堵,而Copilot学生认证又用不了Sonnet/Opus 4.6/4.7,所以没有办法只能用Gemini/Codex,贫穷的大学生啊)
  • 体会:可以看到,现在的大模型仍然不能完全替代我们的编码过程,尤其是他的Debug能力(也可能和我的Debug提示词写的太弱有关)。但是等到大模型完全有能力独自解决编码过程中的所有问题的时候,人类在其中的地位该会是怎样的,似乎真的值得我们深思。但至少可以肯定的是,现阶段,我们的思考,我们的决策,是在AI时代仍然重要的能力。

体验与感受

久闻OO电梯,也是终于亲身体验了。我最大的感受一个是指导书太长了,每次读都有种无从下手的感觉,(幸好AI辅助我阅读理解),一个是,我再也不因为抢不到票而骂系统做的烂了…… 总而言之,庆祝自己完成了电梯单元!