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

王一舟-24371515 2026-04-28 11:33:18

 

一、 同步块的设置、锁的选择与处理语句的关系

在三次迭代中,随着电梯数量增加、重置机制引入以及最终双轿厢系统的加入,同步机制的设计经历了从粗放到精细的过程。

1. 锁的选择与对象设计

  • 第一、二次作业(基础锁): 主要采用 synchronized 关键字修饰方法。锁的对象通常是共享的数据结构(如 GlobalQueue 和电梯内部的 RequestTable)。这种选择实现简单,能够有效保证生产者(输入线程)和消费者(调度器/电梯线程)之间的线程安全。

  • 第三次作业(细粒度与互斥锁): 引入双轿厢后,单纯的队列锁不足以解决物理空间的互斥问题。在换乘层(F2)的设计中,引入了 ElevatorShaft(电梯井)作为独立对象,并利用 synchronized 配合 wait()/notifyAll() 实现了针对特定楼层的互斥锁,保证两部轿厢不会发生物理碰撞。

2. 同步块与处理语句的关系

  • 缩小临界区(Critical Section): 核心原则是**“只在必要时加锁,且尽快释放”**。例如,电梯的移动(Thread.sleep 模拟的耗时操作)和输出(TimableOutput绝对不能放在持有共享队列锁的同步块中,否则会导致整个系统卡顿,甚至引发超时(TLE)。

  • 状态读取与更新: 在同步块中,应当只进行快速的状态读写操作(如判断队列是否为空、添加/删除请求、修改电梯状态)。

  • 防御性拷贝: 在遍历队列分配任务时,为了避免 ConcurrentModificationException,常常在同步块中克隆一份列表副本,然后在锁外部进行耗时的逻辑判断。


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

1. 调度器与线程的交互机制

  • 层级架构: 形成了 InputThread -> GlobalQueue -> Dispatcher -> LocalQueue -> Elevator 的流水线结构。

  • 交互方式(Wait-Notify 模型): * 输入与调度: Dispatcher 作为一个独立线程运行,持续监控 GlobalQueue。当队列为空时,调度器调用 wait() 陷入阻塞,避免 CPU 轮询造成的“忙等”(Busy Waiting)。InputThread 放入新请求后,调用 notifyAll() 唤醒调度器。

    • 调度与电梯: Dispatcher 唤醒后,根据各个电梯当前的状态和内部队列,将请求分发给特定电梯的 waitQueue,并再次唤醒处于空闲休眠状态的对应电梯。

2. 多维度性能权衡的调度策略

  • 局部策略(LOOK 算法): 单个电梯内部采用 LOOK 算法。电梯在同方向上一直运行直到没有更远的请求,这极大减少了无意义的折返,是优化时间性能运行耗电量的基石。

  • 全局策略(影子电梯/成本估算):Dispatcher 中引入了 evaluateCost() 方法。面对一个新请求,调度器会让每部电梯计算“接单成本”。

    • 时间考量: 估算到达目标楼层的物理距离,并结合当前车内和等候队列的乘客数量(预测停靠开门的次数延迟)。

    • 空间与电量考量: 引入重量评估(curWeight + p.getWeight() <= maxweight),避免超载导致的无效分配。同时,优先分配给同向且顺路的电梯,这不仅缩短了乘客等待时间,也变相减少了电梯总运行里程,降低了系统总电量消耗。


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

多线程的幽灵 Bug 通常在特定的时间交错下才会显现,以下是典型案例与排查思路:

1. 出现过的典型 Bug

  • 无限换乘死循环(逻辑死锁): 在第三次作业的双轿厢模式中,主轿厢在 F2 放下需要继续向下的乘客后,由于成本计算缺陷,调度器又将该乘客分配给了主轿厢,导致电梯在 F2 无限开关门,最终不仅霸占了互斥锁,还导致同井道备用轿厢超时强闯,发生碰撞。

  • 空中变身(并发竞争条件 Race Condition): 这是极其隐蔽的 Bug。主轿厢在空车避让移动(Thread.sleep)的极短时间内,备用轿厢恰好完成了回收任务并强行修改了主轿厢的状态(从 DOUBLE 切回 NORMAL)。导致电梯到达目标楼层时状态非法,被评测机判定为违规移动。

2. 高效 Debug 方法

  • 打印调试法(Printf Debugging): 在关键状态节点(如 wait() 前后、锁获取/释放时、状态改变时)输出当前线程名、时间和具体状态参数。通过观察日志的先后顺序,还原“案发现场”。

  • 梳理状态机模型: 多线程容易出问题往往是因为状态流转不严谨。通过画出所有可能的状态迁移图,检查是否存在“非法跃迁”(如在移动状态下被修改为静止状态)。

  • 引入中间状态标志位: 如针对“空中变身”Bug,引入 isMoving 标志位。在执行物理耗时动作前后加锁修改标志,并在外部强制状态切换时增加 while(isMoving) wait() 的防御机制。

  • 本地自动化压测: 编写 Python 等脚本生成大量极端随机数据,尤其是针对边界楼层(如 F2)、密集同层请求等,利用高频次运行来“逼出”由于极小概率时间交错导致的线程安全问题。


四、 对线程安全和层次化设计的深刻理解

1. 线程安全(Thread Safety)的本质

线程安全不是简单地到处加 synchronized。过度加锁会导致系统退化为单线程串行,甚至引发死锁;而加锁不足则会导致脏读、脏写。

  • 封装共享资源: 核心在于保护不变性(Invariants)。例如,队列的长度和内容必须保持一致。将共享资源(如 QueueRequestTable)封装成独立的线程安全类,对外只暴露安全的 addget 方法,屏蔽了内部的锁机制。

  • 防止死锁(Deadlock): 避免嵌套持有多把锁。如果必须持有多把锁,必须保证全局所有线程获取锁的顺序绝对一致(例如总是先获取 GlobalQueue 锁,再获取 Shaft 锁)。

2. 层次化设计(Hierarchical Design)的魅力

这三次作业是面向对象设计的极佳实践,良好的层次划分让复杂的并发逻辑变得可控:

  • 高内聚低耦合: InputThread 只管解析,不管怎么运;Dispatcher 只管算账分发,不管怎么跑;Elevator 只管拉人跑楼,不管请求怎么来。这种解耦使得在增加重置机制和双轿厢机制时,核心的逻辑模块不需要推倒重来。

  • 面向接口与抽象编程: 将请求统一抽象为 Request,使得调度策略能够统一处理 PersonRequestMaintRequest 等不同类型的任务。

  • 状态模式(State Pattern)的雏形: 在电梯内部使用 enum State 管理 NORMALREPAIRDOUBLE 等复杂状态,使得 run() 方法中的逻辑清晰明了,不同状态的流转被严格约束在各自的处理函数中,极大地提升了代码的可维护性和可读性。

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

309

社区成员

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

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