OO-Unit2总结

薛傲祖-24373517 2026-04-24 15:55:31

前言

在面向对象第二单元中,我们从零开始完成了一个多线程实时电梯调度系统,经历了单电梯固定分配多电梯竞争调度+维修双电梯协同+更新/回收三个递进式作业。本文将从同步块与锁的选择、调度器设计、Debug经历、线程安全与层次化设计、大模型使用心得等方面,对整个单元进行系统总结。


一、同步块与锁的选择分析

1. 第一次作业:最简单的 wait/notify 模型

第一次作业只有 6 部独立电梯,输入直接指定电梯 ID,电梯本身只有 NORMAL 状态。同步方式非常朴素:

  • 对 waitingPeople 的添加使用 synchronized addPerson(),并在方法内 notifyAll()

  • 电梯主循环中,使用 synchronized(this) 包裹 while(isEmpty() && !isStopped) wait()

  • 其他共享变量如 currentFloorpassengersweight 等均使用 volatile 修饰。

锁与处理语句的关系this 锁保护的临界区是“等待队列为空且未停止”的条件等待。当调度器线程调用 addPerson() 时,会打破空等条件并唤醒电梯线程,保证线程间的协作。但由于所有与电梯状态相关的方法(如 openProcessmoveOneStep)并没有全部置于同一把锁下,只是使用了 volatile 保证了可见性,原子性依赖的事实上是“电梯运行时只有自身线程修改状态”这一隐含假设。

2. 第二次作业:状态机引入与更复杂的同步块

第二次作业加入了维修请求,电梯多了 REP_ACCEPTREPAIRTEST 等状态。同步机制升级:

public synchronized void addMaintRequest(Person person, String target) {
    this.haveMainTask = true;
    status = Status.REP_ACCEPT;
    notifyAll();
}
  • run() 中的 synchronized(this) 块扩大,增加了状态判断(status != NORMAL && haveMainTask 时直接 startMaint())。

  • 调度器增加了 allocLock,用于在无可用电梯时阻塞请求线程。

锁的层次

  • this 锁负责电梯内部线程的阻塞/唤醒,所有修改请求队列或状态并需要唤醒电梯的方法均需持有 this

  • allocLock 是调度器级别的锁,用于解决“所有电梯都在维修,新请求暂时无法分配”的问题,让请求线程等待直到有电梯恢复正常。

这种设计已经体现出“每个对象一个锁,各管各的状态”的层次化思想。

3. 第三次作业:双电梯协同与多锁协作

第三次作业实现了主电梯(F2~F7)+ 副电梯(B4~F2)+ 中间换乘,并引入了更新回收请求。这带来了更复杂的并发场景:

  • shaftLock:主副电梯在 F2 共用井道,moveOneStep 中当 currentFloor == 1 || targetFloor == 1 时,必须持有 shaftLock 才能移动,避免碰撞。

  • 双电梯状态联动startUpdate() 完成后设置 partner.setNeedsUpdate(true) 并 notifyAll(),使伙伴电梯感知到更新完成并切换为 DOUBLE 模式。

  • 分配锁优化Scheduler.allocElevator() 中加入 canServe() 判断,确保请求只能分配给真正能到达该楼层的电梯。无可用电梯时仍在 allocLock 上等待。

锁与处理语句的关系总结

锁对象保护的数据/条件相关操作
this(电梯对象)waitingPeoplestatushaveMainTask 等状态变量addPersonaddMaintRequest、主循环 wait
allocLock电梯资源池可用性请求分配时的等待/通知
shaftLock主副电梯在 F2 的物理互斥跨越 F2 的移动

所有锁都遵循“谁的数据谁保护”原则,锁内操作尽量简短,避免嵌套锁死锁(shaftLock 只用于一步移动,不跨状态)。


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

1. 调度器架构演变

第一次:调度器退化成一个接收器——请求自带电梯 ID,直接 addPerson。无调度算法。

第二次:调度器实现最小负载优先分配策略:

public int allocElevator() {
    int minLoad = Integer.MAX_VALUE, num = 0;
    for (int i = 1; i < 7; i++) {
        if (!elevators[i].inRepair() && elevators[i].getLoad() < minLoad) {
            minLoad = elevators[i].getLoad();
            num = i;
        }
    }
    return num;
}

当所有电梯维修时,请求线程在 allocLock 上等待。

第三次:引入分区调度 + 负载均衡

public boolean canServe(Elevator elevator, int from, int to) {
    if (elevator.isMain()) {
        if (elevator.getStatus() == Status.DOUBLE) return from >= 1 && !(from == 1 && to < 1);
        else if (elevator.getStatus() == Status.NORMAL) return true;
    } else {
        if (elevator.getStatus() == Status.DOUBLE) return from <= 1 && !(from == 1 && to > 1);
    }
    return false;
}

只有可达的电梯才参与竞争,同时结合负载(getLoad() < 15)和在线状态(inShaft),保证请求总能被正确服务。

线程交互

  • InputHandler 线程解析输入,调用 scheduler.addRequest()

  • Scheduler 线程(实际是调用者线程,即 InputHandler 或 Elevator)负责分配电梯,可能阻塞在 allocLock

  • Elevator 线程执行 RUN 循环,完成移动、开关门、维修/更新/回收。

  • 电梯完成特殊操作后调用 scheduler.notifyAvailable() 唤醒等待分配的线程。

2. 调度策略与性能指标

电梯内部采用LOOK 算法(单向扫描到底再折返):

public int getTargetFloor() {
    // 找出同方向最近的目标楼层
    // 如果同方向无请求,反转方向寻找最近目标
}

对时间的影响:LOOK 算法避免了频繁折返,在请求密集时能明显降低平均等待时间。

对电量的影响:减少无意义的反向空驶,直接降低总移动楼层数,进而节省时间与电量。
第三次作业进一步通过主副电梯分区:原来一部电梯要跑 B4~F7,现在各管一半,长距离跨区请求通过 F2 换乘实现,总行程大幅缩短,电量优化明显。

此外,调度器优先分配给负载最小的电梯,使各电梯任务均衡,从全局角度进一步降低了拥堵和单梯过载导致的长时间等待。


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

1. 典型 Bug:并发修改异常

在阅读自己和他人代码时,发现一个高频问题是 ConcurrentModificationException。典型场景如下:

// 在 Elevator.openProcess() 中
Iterator<Person> it = passengers.iterator();
while (it.hasNext()) { ... it.remove(); }

而 addPerson() 或 flushWait() 可能在其他线程中修改 passengers 或 waitingPeople 列表。由于这些列表是 ArrayList 且没有在遍历时加锁,在强测的大规模随机测试中有概率抛出并发修改异常。

修复方案

  • 将遍历和修改操作统一在 synchronized(this) 块中,保证互斥。

  • 或改用 CopyOnWriteArrayList,但会增加开销,不如规范同步。

另一个隐患是 volatile 不足以保护 ++ / -- 复合操作,但我的 currentFloor 只在电梯自身线程中修改,因此仅用 volatile 保证可见性就足够。

2. 状态机死锁与忙等

在第三次作业中,主副电梯相互唤醒的逻辑容易导致丢失信号或死等。例如更新完成后,必须 partner.notifyAll(),并且接收方需要 checkChange() 及时处理 needsUpdate 标志。若一方在 wait() 时未检查标志就进入等待,可能永久阻塞。我通过在每次 wait 返回后立即调用 checkChange() 解决。

3. Debug 方法

  • 日志分析法:利用 TimableOutput 输出的时间戳日志,手工或脚本检查事件顺序,比如 RECEIVE -> IN -> OUT 是否匹配,开关门时间是否满足 400ms。

  • 断言与边界测试:在关键位置插入 assert,如 weight >= 0 && weight <= 400,一旦违反立即发现。

  • 多线程测试:编写模拟输入,反复运行,看是否会偶发异常。


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

1. 线程安全

这三次作业让我深刻体会到,线程安全不是靠 volatile 和 synchronized 乱加就能保证的。必须:

  • 明确每一份共享数据的所有者线程访问者线程

  • 所有访问共享数据的代码路径必须一致地持有同一把锁

  • 尽量将复合操作(如“检查-执行”)原子化,例如 isEmpty() 判断加任务获取,必须在同步块内完成。

  • 避免在持有锁的情况下调用外部可能获取另一把锁的方法,防止死锁。

2. 层次化设计

从第一次到第三次,程序层次越来越清晰:

  • 输入层:只负责解析,不涉及业务逻辑。

  • 调度层:负责请求分配、全局状态感知(维修/更新/回收),对外提供 notifyAvailable

  • 执行层:电梯只关心自己的队列、状态机、移动和开关门。

这种分层让每层的变更相对独立。第三次作业加入更新和回收时,调度层增加 canServe 和 deallocate,电梯层增加相应的状态处理和 checkChange,双方通过明确的方法调用交互,没有破坏原有逻辑。


五、大模型使用心得

模型与分工

我主要使用 Claude Opus 4.7 和 GitHub Copilot
分工方式:我主导架构设计、状态机设计和并发模型;大模型负责辅助代码生成、Bug 解释、测试用例编写

优势

  1. 快速生成框架代码:如 FloorMapperPerson 类等重复性工作,几乎可以一键生成。

  2. 多线程语法纠正:能够指出 wait() 必须在 synchronized 块中使用等细节。

  3. 性能优化建议:提供 LOOK 算法伪代码,减少了我查资料的时间。

困难

  1. 并发逻辑理解肤浅:模型给出的多线程代码常常“看起来正确”,但存在隐藏的竞态条件,必须自己逐行分析。

  2. 上下文遗忘:在长对话中,模型有时忘记之前定义的锁策略,产生不一致代码。

  3. 无法运行验证:大模型只能静态分析,无法实际运行强测,最终的线程安全仍然依赖自己的推理和测试。

感受

大模型像一位 7x24 小时在线的代码助手,能极大加速“写代码”的环节,但“设计对不对”这个核心问题依然需要自己的专业判断。尤其在多线程这类非确定性 BUG 高发领域,人必须掌握底层并发模型,才能驾驭 AI 的输出


六、第二单元体验与建议

体验

第二单元是多线程电梯,从一个看似简单的“电梯上下”开始,逐步加入维修、多梯竞争、分区协同、状态机……每一步都让我对并发编程的理解更加深刻。

建议

  1. 希望提供更完善的本地评测工具:能模拟随机时序,暴露更多并发问题。

  2. 研讨课增加“互测 Bug 回顾”环节:让同学分享那些最难发现的线程安全或者死锁问题,集体学习。

  3. 作业指导书中可提前强调“集合遍历必须同步”:虽然这应该是基础知识,但在多线程环境中新人极容易忽略。


第二单元结束,我因多线程行为难以理解预测感到痛苦,也对写出一套稳定运行的多线程程序充满成就感。感谢课程组的精心设计!

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

305

社区成员

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

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