309
社区成员
发帖
与我相关
我的任务
分享第五次作业是首次接触多线程,整体架构采用了最经典的生产者-消费者模式。输入线程(InputThread)和调度器(Dispatcher)作为生产者,将请求放入 LinkedBlockingQueue<ElevatorTask>;六部电梯线程作为消费者,从各自的队列中取出任务。
// Dispatcher.java
elevatorQueues[index].put(task);
// ElevatorThread.java
ElevatorTask task = queue.take();
锁的选择:全程依赖 BlockingQueue 的内部锁(ReentrantLock 的两个条件队列),没有显式使用 synchronized 块。这种设计的优点是实现简单、线程安全由 JDK 保证,避免了手动加锁的复杂度;缺点是电梯只能被动等待队列,无法主动感知外部状态变化。
第六次作业引入了临时检修和电梯状态机(NORMAL → REP_ACCEPT → REPAIR → TEST),调度器需要实时感知电梯状态以避开正在检修的电梯。因此架构上新增了 ElevatorStatus 和 StatusHandler 线程,电梯通过额外的 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 埋下伏笔。
第七次作业进行了彻底重构,放弃了第六次作业中复杂的状态汇报队列,改为更清晰的共享对象模型。
锁的演进:
ElevatorQueue 层:所有方法使用 synchronized 方法锁,内部通过 wait()/notifyAll() 实现电梯线程的阻塞与唤醒。
public synchronized void add(Passenger p) {
waiting.add(p);
notifyAll();
}
public synchronized void await() { wait(); }
Dispatcher 层:全局待分配乘客池 pool 使用 synchronized (this) 保护,保证 addPassenger() 和 run() 中的批量消费互斥。
Elevator 层:getInsideCount() 等查询方法使用 synchronized 方法锁,供调度器在评分时安全读取。
F2 碰撞锁:双轿厢模式下,主/备轿厢不能同时处于 F2。引入了一个独立的 Object f2Lock,在 moveOne() 和 chooseAndMove() 中通过 synchronized (f2Lock) 进行临界区保护,并使用 wait(100) 实现避让等待。
synchronized (f2Lock) {
if (pair.getCurrentFloor() == Floor.F2) {
// 等待另一轿厢离开
}
}
设计反思:第七次作业通过分层加锁(队列锁、调度器锁、碰撞锁)替代了第六次作业的集中式大锁,降低了线程竞争粒度。wait/notify 的引入也彻底消除了轮询,使电梯在无事可做时真正阻塞。
| 作业 | 调度器职责 | 与线程交互方式 |
|---|---|---|
| HW5 | 透传分配器:仅按输入指定的电梯 ID 转发请求 | 单队列分发,无状态感知 |
| HW6 | 状态感知调度器:需避开检修电梯,处理 RECEIVE 重分配 | 双向通信(Dispatcher → 电梯队列 + 电梯 → StatusHandler 状态队列) |
| HW7 | 全局评分调度器:统筹 6 个井道(12 个轿厢),支持换乘 | 统一乘客池 + 独立电梯队列,调度器批量消费后分配 |
HW7 的 Dispatcher 是调度逻辑最复杂的一次。它维护了一个全局 pool,输入线程不断向其中投放乘客;调度器线程批量取出后,根据当前所有电梯的实时状态进行评分分配。
单轿厢策略(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)接力。这种设计以换乘延迟换取整体吞吐量,在高峰时段能充分利用两个轿厢的并行能力。
性能指标适应:
轮询导致 CPU 时间超限(HW6)
第六次作业中,为了及时捕获 MAINT 请求,电梯在 moveTo() 的每层移动间都使用 queue.poll()。虽然意图正确,但结合某些边界条件,电梯在空闲时也会高频轮询,导致总 CPU 时间超过 10s 限制。修复:第七次作业完全改用 wait/notify,电梯无事时阻塞在 queue.await()。
检修状态 RECEIVE 未强制结束(HW6)
指导书要求输出 MAINT1-BEGIN 时该电梯所有 RECEIVE 必须结束。早期实现中,电梯内乘客强制下电梯(OUT-F)后,调度器未能及时将 assigned[] 中对应的请求清除,导致检修期间仍有未结束 RECEIVE。修复:通过 ElevatorStatus.Event.MAINT1 事件通知 StatusHandler,由调度器统一 forceEndReceive 并重新分配。
双轿厢 F2 碰撞与死等(HW7)
双轿厢模式下,若主轿厢和备用轿厢同时试图进入 F2,会产生死锁或活等。早期使用 Thread.sleep(50) 忙等,效率低下且不安全。修复:引入 f2Lock,在移动前检查对位轿厢位置,若冲突则 wait(),离开 F2 时 notifyAll()。
换乘乘客重复计数(HW7)
一个乘客被拆分为两段后,dispatcher.passengerDone() 在最终到达时才应调用。早期实现中,第一段到达 F2 输出 OUT-F 时若误调用 passengerDone(),会导致 unfinished 计数提前归零,系统提前结束。修复:仅在 OUT-S(最终到达目标层)时调用 passengerDone()。
三次作业让我深刻体会到:线程安全不仅仅是加锁,更是关于数据所有权的设计。
BlockingQueue,电梯线程不共享可变状态,是最安全的模型。synchronized,但集中式大锁导致性能瓶颈和逻辑耦合。ElevatorQueue、Dispatcher 的 pool),让每个对象自己管理并发访问,实现了更清晰的线程安全边界。关键原则:尽量缩小锁的粒度,尽量让锁保护的数据与行为封装在同一类中(如 ElevatorQueue 自己管理 waiting 列表),避免跨对象的裸同步。
三次作业的架构演进体现了层次化设计的重要性:
InputThread 负责解析和投递。Dispatcher 负责全局决策,不关心电梯如何移动。Elevator 负责执行移动和开关门,通过 ElevatorQueue 与调度层解耦。Floor、Passenger 等提供领域模型支持。这种“调度器只发指令,电梯只管执行”的单向依赖,使得双轿厢、检修等复杂功能能够在执行层局部扩展,而不影响调度层的整体逻辑。
在本单元中,我主要将大模型(Kimi)作为架构顾问和代码审查员,而非直接的代码生成器。具体分工如下:
chooseAndMove、doMaintenance)贴给模型,让其检查是否违反指导书约束(如“REPAIR 状态禁止移动”)。优势:
wait/notify 的配对或锁的释放路径,模型能从代码中静态分析出潜在的死锁或竞态条件。困难:
Thread.sleep(1000) 而未考虑与 TimableOutput 的协同,需要人工精细调整。大模型在多线程电梯这种状态多、约束杂、边界多的场景下,更像是一个“不知疲倦的助教”。它不能替代你写核心架构,但能在你卡住时提供思路、在你写完时代码审查、在你测试时构造样例。关键是使用者自己必须先建立清晰的设计意图,否则很容易被模型带偏到错误的同步方案上。
第二单元是我认为 OO 课程中最具挑战性、也是收获最大的一个单元。从第一次接触 synchronized 和 wait/notify 时的茫然,到第七次作业能够独立设计一个支持双轿厢、检修、换乘的复杂多线程系统,这个过程中敬畏感逐渐转化为掌控感。