O.o第二单元电梯总结

于忠雨-23371320 2026-04-25 22:08:20

2026面向对象设计与构造第二单元总结

一、同步块的设置和锁的选择

1.1 第五次作业:基于 BlockingQueue 的生产者-消费者模式

第五次作业是首次接触多线程,整体架构采用了最经典的生产者-消费者模式。输入线程(InputThread)和调度器(Dispatcher)作为生产者,将请求放入 LinkedBlockingQueue<ElevatorTask>;六部电梯线程作为消费者,从各自的队列中取出任务。

// Dispatcher.java
elevatorQueues[index].put(task);

// ElevatorThread.java
ElevatorTask task = queue.take();

锁的选择:全程依赖 BlockingQueue 的内部锁(ReentrantLock 的两个条件队列),没有显式使用 synchronized 块。这种设计的优点是实现简单、线程安全由 JDK 保证,避免了手动加锁的复杂度;缺点是电梯只能被动等待队列,无法主动感知外部状态变化。

1.2 第六次作业:显式 synchronized 与状态机同步

第六次作业引入了临时检修电梯状态机NORMALREP_ACCEPTREPAIRTEST),调度器需要实时感知电梯状态以避开正在检修的电梯。因此架构上新增了 ElevatorStatusStatusHandler 线程,电梯通过额外的 BlockingQueue<ElevatorStatus> 向调度器汇报状态。

// Dispatcher.java 中的 StatusHandler
synchronized (Dispatcher.this) {
    floors[s.id - 1] = s.floor;
    states[s.id - 1] = s.state;
    if (s.event == ElevatorStatus.Event.MAINT1) {
        forceEndReceive(s.id);
    }
}

锁的选择:在 Dispatcher 中使用了 synchronized (Dispatcher.this) 保护共享的状态数组(states[]floors[]receiving[]assigned[])。这里使用类对象锁是因为 StatusHandler 是 Dispatcher 的内部类,需要与外部类的 assign() 方法互斥访问共享数据。

存在的问题:为了及时响应检修请求,电梯线程在 moveTo()processNormalOperation() 中使用了 queue.poll() 进行非阻塞轮询。虽然意图是不错过 MAINT 指令,但这实际上破坏了生产者-消费者的优雅阻塞模型,导致 CPU 空转风险,也为后续 Bug 埋下伏笔。

1.3 第七次作业:细粒度锁与 wait/notify

第七次作业进行了彻底重构,放弃了第六次作业中复杂的状态汇报队列,改为更清晰的共享对象模型。

锁的演进

  1. ElevatorQueue 层:所有方法使用 synchronized 方法锁,内部通过 wait()/notifyAll() 实现电梯线程的阻塞与唤醒。

    public synchronized void add(Passenger p) {
        waiting.add(p);
        notifyAll();
    }
    public synchronized void await() { wait(); }
    
  2. Dispatcher 层:全局待分配乘客池 pool 使用 synchronized (this) 保护,保证 addPassenger()run() 中的批量消费互斥。

  3. Elevator 层getInsideCount() 等查询方法使用 synchronized 方法锁,供调度器在评分时安全读取。

  4. F2 碰撞锁:双轿厢模式下,主/备轿厢不能同时处于 F2。引入了一个独立的 Object f2Lock,在 moveOne()chooseAndMove() 中通过 synchronized (f2Lock) 进行临界区保护,并使用 wait(100) 实现避让等待。

    synchronized (f2Lock) {
        if (pair.getCurrentFloor() == Floor.F2) {
            // 等待另一轿厢离开
        }
    }
    

设计反思:第七次作业通过分层加锁(队列锁、调度器锁、碰撞锁)替代了第六次作业的集中式大锁,降低了线程竞争粒度。wait/notify 的引入也彻底消除了轮询,使电梯在无事可做时真正阻塞。


二、调度器设计与调度策略

2.1 调度器架构演进

作业调度器职责与线程交互方式
HW5透传分配器:仅按输入指定的电梯 ID 转发请求单队列分发,无状态感知
HW6状态感知调度器:需避开检修电梯,处理 RECEIVE 重分配双向通信(Dispatcher → 电梯队列 + 电梯 → StatusHandler 状态队列)
HW7全局评分调度器:统筹 6 个井道(12 个轿厢),支持换乘统一乘客池 + 独立电梯队列,调度器批量消费后分配

HW7 的 Dispatcher 是调度逻辑最复杂的一次。它维护了一个全局 pool,输入线程不断向其中投放乘客;调度器线程批量取出后,根据当前所有电梯的实时状态进行评分分配

2.2 调度策略与性能适应

单轿厢策略(HW5/HW6):电梯内部采用类 LOOK 策略。电梯优先运送同方向乘客,若前方无目标则反向。chooseNextFloor() 选择距离当前楼层最近的目标楼层(先送轿厢内乘客,再接等待乘客)。

全局分配策略(HW7):调度器使用评分函数选择最优电梯:

private int score(Elevator e, int from) {
    return Floor.distance(e.getCurrentFloor(), from)
         + e.getInsideCount() * 3
         + e.getQueue().size() * 2;
}
  • 距离项:优先选择离出发层近的电梯,减少空驶时间。
  • 负载项insideCount * 3 + queueSize * 2 惩罚高负载电梯,避免局部过载。

双轿厢与换乘:HW7 引入了换乘层 F2。若乘客请求跨越主/备轿厢的运行范围(如从 B1 到 F5),调度器会将其拆分为两段:先由备用轿厢(B4-F2)运至 F2,再由主轿厢(F2-F7)接力。这种设计以换乘延迟换取整体吞吐量,在高峰时段能充分利用两个轿厢的并行能力。

性能指标适应

  • 运行时间:通过 LOOK 策略减少无效移动,通过评分分配均衡负载。
  • 平均完成时间:优先分配近距离电梯,减少乘客等待。
  • 耗电量:避免频繁开关门和空跑;双轿厢模式下主/备轿厢各司其职,减少大范围移动。

三、Bug 分析与多线程 Debug 方法

3.1 遇到的典型 Bug

  1. 轮询导致 CPU 时间超限(HW6)
    第六次作业中,为了及时捕获 MAINT 请求,电梯在 moveTo() 的每层移动间都使用 queue.poll()。虽然意图正确,但结合某些边界条件,电梯在空闲时也会高频轮询,导致总 CPU 时间超过 10s 限制。修复:第七次作业完全改用 wait/notify,电梯无事时阻塞在 queue.await()

  2. 检修状态 RECEIVE 未强制结束(HW6)
    指导书要求输出 MAINT1-BEGIN 时该电梯所有 RECEIVE 必须结束。早期实现中,电梯内乘客强制下电梯(OUT-F)后,调度器未能及时将 assigned[] 中对应的请求清除,导致检修期间仍有未结束 RECEIVE。修复:通过 ElevatorStatus.Event.MAINT1 事件通知 StatusHandler,由调度器统一 forceEndReceive 并重新分配。

  3. 双轿厢 F2 碰撞与死等(HW7)
    双轿厢模式下,若主轿厢和备用轿厢同时试图进入 F2,会产生死锁或活等。早期使用 Thread.sleep(50) 忙等,效率低下且不安全。修复:引入 f2Lock,在移动前检查对位轿厢位置,若冲突则 wait(),离开 F2 时 notifyAll()

  4. 换乘乘客重复计数(HW7)
    一个乘客被拆分为两段后,dispatcher.passengerDone() 在最终到达时才应调用。早期实现中,第一段到达 F2 输出 OUT-F 时若误调用 passengerDone(),会导致 unfinished 计数提前归零,系统提前结束。修复:仅在 OUT-S(最终到达目标层)时调用 passengerDone()


四、线程安全与层次化设计

4.1 线程安全理解

三次作业让我深刻体会到:线程安全不仅仅是加锁,更是关于数据所有权的设计

  • HW5 的线程安全完全委托给 BlockingQueue,电梯线程不共享可变状态,是最安全的模型。
  • HW6 引入了共享状态数组,被迫使用 synchronized,但集中式大锁导致性能瓶颈和逻辑耦合。
  • HW7 通过将共享数据封装到独立对象ElevatorQueueDispatcherpool),让每个对象自己管理并发访问,实现了更清晰的线程安全边界。

关键原则:尽量缩小锁的粒度,尽量让锁保护的数据与行为封装在同一类中(如 ElevatorQueue 自己管理 waiting 列表),避免跨对象的裸同步。

4.2 层次化设计理解

三次作业的架构演进体现了层次化设计的重要性:

  • HW5:输入 → 调度器 → 电梯,三层清晰但调度器过于单薄。
  • HW6:试图在电梯内部实现复杂状态机,同时调度器又要感知状态,导致双向依赖,层次混乱。
  • HW7:明确划分:
    • 输入层InputThread 负责解析和投递。
    • 调度层Dispatcher 负责全局决策,不关心电梯如何移动。
    • 执行层Elevator 负责执行移动和开关门,通过 ElevatorQueue 与调度层解耦。
    • 工具层FloorPassenger 等提供领域模型支持。

这种“调度器只发指令,电梯只管执行”的单向依赖,使得双轿厢、检修等复杂功能能够在执行层局部扩展,而不影响调度层的整体逻辑。


五、大模型使用心得

5.1 分工方式

在本单元中,我主要将大模型(Kimi)作为架构顾问和代码审查员,而非直接的代码生成器。具体分工如下:

  • 我负责:核心架构设计、状态机转移逻辑、评分函数、同步策略决策。
  • 模型负责
    1. 代码走查:将单个方法(如 chooseAndMovedoMaintenance)贴给模型,让其检查是否违反指导书约束(如“REPAIR 状态禁止移动”)。
    2. 提示词工程:在 HW6/HW7 遇到 Bug 时,我会整理当前现象、相关代码片段和指导书约束,让模型生成给 Copilot 的修复提示词(Prompt)。
    3. 边界样例生成:让模型根据指导书约束构造极端测试样例(如“双轿厢同时到 F2”、“检修时电梯满载”)。

5.2 优势与困难

优势

  • 快速理解复杂约束:指导书中检修、双轿厢的状态机转移规则非常繁琐,模型能快速梳理出“状态 → 行为 → 转移条件”的表格,帮助我建立清晰的心智模型。
  • 多线程逻辑检查:人类容易遗漏 wait/notify 的配对或锁的释放路径,模型能从代码中静态分析出潜在的死锁或竞态条件。
  • Prompt 优化:在 HW7 的 F2 碰撞问题中,我自己难以准确描述 Bug,模型帮我将“现象 + 代码 + 期望行为”整理成结构化的 Copilot 提示词,显著提高了 AI 辅助编程的效率。

困难

  • 上下文长度限制:第七次作业代码文件多、逻辑长,单次对话难以塞入全部代码,只能分段审查,导致模型对全局架构的理解有时出现偏差。
  • 幻觉问题:模型偶尔会“编造”不存在的指导书约束(如虚构“双轿厢不能同时开门”),需要我时刻对照官方文档验证。
  • 时间敏感逻辑:电梯作业对时间戳和 sleep 精度要求极高,模型生成的代码有时使用粗略的 Thread.sleep(1000) 而未考虑与 TimableOutput 的协同,需要人工精细调整。

5.3 使用感受

大模型在多线程电梯这种状态多、约束杂、边界多的场景下,更像是一个“不知疲倦的助教”。它不能替代你写核心架构,但能在你卡住时提供思路、在你写完时代码审查、在你测试时构造样例。关键是使用者自己必须先建立清晰的设计意图,否则很容易被模型带偏到错误的同步方案上。


第二单元是我认为 OO 课程中最具挑战性、也是收获最大的一个单元。从第一次接触 synchronizedwait/notify 时的茫然,到第七次作业能够独立设计一个支持双轿厢、检修、换乘的复杂多线程系统,这个过程中敬畏感逐渐转化为掌控感。

...全文
23 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

309

社区成员

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

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