307
社区成员
发帖
与我相关
我的任务
分享OO第二单元围绕多线程协作展开:从指定乘客目标电梯,到新增维修指令和自主设计分配策略,到最后一次引入双轿厢。课程作业要求要保证线程安全,不会产线程冲突,又要在响应时间与无效移动之间做权衡。本单元的作业难度较大,代码量显著增加,且涉及到多线程会带来意想不到的bug。


RequestPool:以内置锁保护队列与条件等待全局池、井道入站池以及每台轿厢的专用池均复用 RequestPool。所有对 Deque<PersonRequest> 的读写、closed 标志的修改,以及 wait / notifyAll,都落在同一个 synchronized 实例方法上,即以当前 RequestPool 对象为监视器锁。
主要示例:
addGlobalRequest / takeOneRequest:生产者与消费者互斥访问队列;消费者在队列为空且未结束时 wait(),生产者在入队后 notifyAll() 唤醒等待线程。setInputFinished:将 closed 置真并 notifyAll(),使仍在 wait 的线程能读到「结束」语义并返回 null 或空队列,避免永久阻塞。fetchAllUnassigned:一次性拷贝并清空队列,供井道线程批量路由,仍在同一把锁下完成,避免与并发 add 交错导致丢数据或重复。ShaftCoordinator:井道级协作状态与移动前屏障ShaftCoordinator 将 shaftMode、auxEnabled、主/备楼层、shutdown 等放在 synchronized 方法中读写,并在 setShaftMode、setAuxEnabled、updateFloor、beforeMove 等处 notifyAll()。
与块内语句的关系:
waitIfDisabled:备轿厢在「暗室」未启用时,在协调器锁上 wait,直到 setAuxEnabled(true) 或 markShutdown 唤醒,保证启用标志与线程继续执行原子地可见。beforeMove / canMove:移动前在循环中 wait,直到几何约束(双轿厢下主在辅上、不共层、楼层上下界等)满足。锁内维护的是两台轿厢共享的约束条件,语句只读写字段并决定阻塞或放行,不把耗时 IO 放进同步块。ConcurrentHashMap 与 ConcurrentLinkedQueueDispatch 中 elevatorShaftStatusMap使用 ConcurrentHashMap,维保/改造/回收队列使用 ConcurrentLinkedQueue。这类结构用于高频、单操作原子的入队、出队、put,避免与 RequestPool 的监视器锁嵌套;ElevatorStatus 本身是不可变快照,适合被多个线程读取以做全局分配启发式。
小结:锁的选择遵循共享可变状态谁维护、谁持锁——队列一致性由 RequestPool 的monitor保证
本项目的调度是分层的,不是单一中央循环。
Dispatch 调度线程RequestPool 阻塞或轮询取 PersonRequest(与 InputThread 的生产关系为典型的生产者–消费者)。assignElevator 得到目标井道id,将请求写入该井道与ElevatorShaft共享的入站 RequestPool(shaftRequestPools)。setInputFinished(),把「结束」沿流水线向下传递。特殊请求(MaintRequest / UpdateRequest / RecycleRequest)不经过全局乘客池,而是由 InputThread 写入Dispatch侧各 ConcurrentLinkedQueue,由对应主/备轿厢在运行循环中 poll。
ElevatorShaft 线程drain / takeOneRequestWithTimeout 聚合请求,按当前 ShaftCoordinator 模式调用 chooseTargetPool,分流到 **mainPool 或 auxPool**;无法立即分流的进入 deferredRequests 并在后续循环重试。Elevator 线程(shaft-k-main / shaft-k-aux),在本地队列与延迟队列清空且输入结束后 finishSubPools、join 子线程,保证先停源再停消费者的关闭顺序。Elevator 线程与 Decision每台轿厢独立线程从自己的RequestPool 拉请求、receive 进 waitingRequests,再调用 Decision(实现为 LookDecision)得到 OPEN_DOOR / MOVE_* / WAIT / IDLE 等。移动前通过 ShaftCoordinator.beforeMove与井道内另一台轿厢协作。
线程交互一览:
| 线程 | 与其它线程的协作方式 |
|---|---|
input-thread | 写全局池;写 Dispatch 的特殊队列 |
dispatch-thread | 读全局池;写井道入站池;读 ElevatorStatus(ConcurrentHashMap) |
shaft-k | 读入站池;写主/备池;join 轿厢 |
shaft-k-main / aux | 读各自池;reportStatusToDispatch;ShaftCoordinator 上 wait/notify |
assignElevator对每条 PersonRequest:
ElevatorStatus.canTakeRequest 过滤载重不可接的井道(快照中的 currentDispatchWeight 含本机负载、延期、池中与等待队列权重,使全局侧对「是否还能接人」有保守估计)。WAIT/IDLE 的井道,减少「先接人再折返」带来的时间浪费。ThreadLocalRandom 随机打破对称,减轻多井道固定偏向导致的负载不均(间接影响平均等待与个别电梯空驶)。chooseTargetPool在 DOUBLE 模式下以换乘层(实现中为 F2 索引边界)划分低区/高区:低区进副轿厢、高区进主轿厢、跨区默认进主轿厢。
改造(UP_ACCEPT/UPDATE)与回收(REC_ACCEPT/RECYCLE)阶段,chooseTargetPool 返回 null 或收紧可服务楼层,与状态机一致,避免在非法阶段把乘客派到无法服务的轿厢。
LookDecision(LOOK)在已 receive 的 waitingRequests 与车内乘客上执行 LOOK:优先沿当前方向服务同向目标;必要时反向;开门决策结合下客与可上车列表(含载重裁剪与方向一致的挑选)。WAIT 在仍有潜在输入时使用短路径 pullNewRequests 与超时等待配合(见 Elevator 中常量),在及时性与忙等耗电之间折中。
代码调用EventPrint输出关门信息,但是EventPrint中的方法是加锁的,导致在高并发情况下,线程处于等待锁的状态,无法及时输出关门信息。导致输出中实际关门时间与代码中开始等待时间不一致。解决办法,通过调整代码顺序,先输出关门信息,再进入等待状态,确保输出的时间与实际关门时间一致。
当电梯状态发生变化时,电梯会将未完成的请求重新放入全局请求池中,进行重新分配。但是当输入结束时,电梯如果将未完成的请求放入全局请求池中,但由于输入已经结束,调度器不会再从全局请求池中取出请求进行分配,导致这些请求无法被处理。
分配策略的不合理导致超时问题,主要为电梯分配策略只将请求分配给NORMAL状态的电梯,导致在极端情况下,所有请求都被分配给同一台电梯,造成该电梯过载,无法及时处理请求,最终导致超时。解决办法是优化分配策略,将请求分配给所有可用的电梯,而不仅仅是NORMAL状态的电梯,以实现负载均衡。
RequestPool、ShaftCoordinator)与并发集合(队列)Elevator 内 passengers、waitingRequests 等主要由单轿厢线程访问;与调度器的交叉点通过 Dispatch 的线程安全结构与快照上报完成,避免到处加锁。InputThread 只解析与投递,不决定电梯行为。Dispatch 只做哪一口井与特殊请求入队。ElevatorShaft 只做哪一台轿厢的池与延迟重试。Elevator + Decision 管移动与门;ShaftCoordinator 管井道内横切的安全约束。ConcurrentHashMap、ConcurrentLinkedQueue等,以及如何合理使用synchronized块和条件变量来实现线程间的协调。OO第二单元的多线程电梯调度项目是一个综合性的挑战,涉及到线程安全、分层设计、调度策略等多个方面。通过合理使用Java的同步机制和并发数据结构,以及设计清晰的分层架构,实现一个高效且线程安全的电梯调度系统。前两次作业难度适中,但是第三次作业引入了双轿厢和更多的调度细节,情况独特,超越生活的情况导致分配算法不好进行处理。希望可以对第三次作业内容进行修改。