272
社区成员




在面向对象课程的电梯系列作业(第五、六、七次)中,我们逐步构建了一个从简单到复杂的多线程电梯模拟系统。这个过程不仅是对电梯调度逻辑的挑战,更是对多线程编程中线程安全、协作和架构设计的深度实践。本文将总结这三次作业的设计思路、关键技术点以及演进过程。
线程安全是多线程编程的核心问题,其关键在于如何管理对共享资源的并发访问。三次作业中,同步块和锁的使用随着系统复杂度的增加而演进。
第五次作业(单电梯调度):
InputParser
线程向 mainQueue
添加请求,Scheduler
线程从中取出请求并放入对应电梯的 elevatorQueue
,Elevator
线程从自己的 elevatorQueue
取请求并处理。因此,对 RequestQueue
的所有修改操作(addRequest
, getNextRequest
, iterator
操作中的 remove
)以及 wait/notifyAll
都被包裹在 synchronized(this)
或 synchronized(queue)
块中。电梯内部 passengers
列表的修改(boardPassengers
, unboardPassengers
)也需要同步,虽然第五次代码中 boardPassengers
对 elevatorQueue
加锁间接保护了 passengers
的添加,但更严谨的做法是也对 passengers
的访问加锁(如果存在其他并发访问点)。synchronized
关键字)。这种锁简单易用,与 wait/notifyAll
配合,适用于经典的生产者-消费者模式。wait/notifyAll
则用于线程间的通信,当队列为空时消费者(电梯/调度器)等待,当队列有新请求时生产者(输入/调度器)唤醒消费者。第六次作业(多电梯调度 + SCHE):
RequestQueue
的同步保护。新增了 SCHE
指令,电梯在执行 SCHE
期间状态特殊。为此,Elevator
类引入了 onScheLock
对象和 isOnSche
标志位。在判断是否接收请求 (addRequest
) 和电梯主循环决策前检查 isOnSche
时,需要获取 onScheLock
,确保状态判断和后续操作的一致性。sche()
方法开始和结束时修改 isOnSche
也在此锁的保护下。synchronized
为主,并为 SCHE
状态引入了专门的对象锁 onScheLock
,实现了更细粒度的控制。onScheLock
专门用于保护电梯是否处于调度维护状态这一关键标志,防止在维护期间接收新请求或执行普通运行逻辑。其他锁逻辑与第五次作业类似。第七次作业(双轿厢改造 + UPDATE):
RequestQueue
的同步保护依然存在。Elevator
类增加了 onUpdateLock
和 isOnUpdate
标志位,逻辑类似 onScheLock
,用于保护 UPDATE
状态。java.util.concurrent.locks.ReentrantLock
作为 transferLock
。这个锁用于保护双轿厢电梯在 transferFloor
的通行权,防止碰撞。电梯在接近、到达和离开 transferFloor
时需要获取和释放此锁。Update
类内部使用 synchronized(updateLock)
和 wait/notifyAll
机制,协调参与 UPDATE
的两部电梯,确保它们都准备好后才开始执行改造,并在结束后唤醒对方。synchronized
和 ReentrantLock
。synchronized
用于保护队列、电梯内部状态标志。ReentrantLock
用于实现更灵活的锁操作,特别是跨方法的锁获取与释放(虽然这里主要在 move
方法内管理),并且其可重入性有时也很有用(虽然在此场景非关键)。Update
类中的 updateLock
使用对象监视器锁协调两个特定线程。synchronized(queue)
保护请求队列数据。synchronized(onScheLock/onUpdateLock)
保护电梯是否处于特殊模式的状态标志。ReentrantLock (transferLock)
保护关键物理资源(换乘层通道),防止两个电梯同时穿越。synchronized(updateLock)
in Update
协调两个电梯线程的同步点,确保 UPDATE
操作的原子性和双方的同时性。调度器(Scheduler)在系统中扮演着中央协调者的角色,负责将外部请求合理地分配给电梯。
调度器设计:
mainQueue
和各个 elevatorQueue
之间的桥梁。从 mainQueue
获取请求,根据请求指定的 elevatorId
,直接将请求放入对应的 elevatorQueue
。PersonRequest
,它不再根据指定 ID 分配,而是实现了一种简单的负载均衡策略:选择当前已接收请求数(包括电梯内乘客和等待队列请求)最少的电梯进行分配。对于 ScheRequest
,则直接根据其 elevatorId
分配给目标电梯。调度器还需要在输入结束且所有请求(包括 SCHE
)处理完毕后,通知所有电梯结束运行。PersonRequest
。calc
方法综合考虑了多个因素:couldReceivePassenger
),以及请求是否需要换乘 (needsToTransfer
)。SCHE
或 UPDATE
状态。ScheRequest
和 UpdateRequest
,仍然是直接分配给指定电梯。调度器同样负责判断系统终止条件,现在需要考虑 passenger
, SCHE
, 和 UPDATE
请求都完成。与线程的交互:
InputParser
): 生产者,产生请求放入 mainQueue
,并通过 notifyAll
唤醒可能在 mainQueue
上等待的调度器。Scheduler
): 消费者(mainQueue
)和生产者(elevatorQueue
或直接调用电梯 addRequest
方法)。它从 mainQueue
获取请求,处理后分发给电梯线程。当 mainQueue
为空时,调度器 wait()
。Elevator
): 消费者(自己的请求队列/接收到的请求)。接收调度器分配的请求,并根据内部策略运行。当无事可做时,在自身的 elevatorQueue
上 wait()
。Main
): 创建并启动所有线程(输入、调度器、电梯)。调度策略与性能指标:
AdaptiveLookStrategy
)。电梯沿一个方向移动,响应当前方向上的所有请求,到达最远请求后反向。简单高效,主要关注响应时间。SCHE
请求具有高优先级,电梯必须先完成 SCHE
,这会暂时牺牲普通乘客的效率。-3
分),因为换乘增加总时间。SCHE
/UPDATE
状态的电梯也避免了中断带来的额外能耗。速度快的电梯虽然单次移动耗时少,但评分公式中速度项 - (double) speed / 100
表明速度越快评分越低(可能是为了平衡负载或假设高速更耗电,但系数较小)。receivedRequestsNums
因子仍在起作用,避免单个电梯过载。AdaptiveLookStrategy
增加了处理换乘的逻辑,在换乘层会判断是否需要让乘客下电梯换乘。总的来说,调度策略从简单分配到负载均衡,再到复杂的启发式评分,逐步适应了更复杂的场景和多维度的性能考量。尤其在 HW7,策略试图在时间、(间接的)电量、负载之间找到一个较好的平衡点。
第五次作业架构:
第六次作业架构:
第七次作业架构:
三次作业架构设计的逐步变化:
ElevatorStrategy
接口将电梯运行策略分离。Sche
。Update
请求类型和相应的处理逻辑。Elevator 状态管理更复杂(增加了楼层限制、换乘逻辑、UPDATE
状态)。引入了 ReentrantLock
进行更精细的同步控制。Scheduler 的调度算法变得非常核心和复杂。整体上看,架构保持了分层和分离关注点的特点:输入解析、中央调度、电梯执行、运行策略是相对独立的模块。这种设计使得每次迭代时,可以在不大规模重构的情况下,修改或增强特定模块的功能。
未来扩展能力:
Elevator
子类(如果需要不同行为)或通过配置(如容量、速度)创建不同实例。Scheduler
的 schedulePassengerRequest
方法是核心,可以替换其中的评分逻辑以实现不同的优化目标(如最短等待时间优先、能耗最低优先等)。ElevatorStrategy
接口即可更换电梯的内部运行逻辑(如 SCAN、SSTF 等)。Elevator
类增加相应接口和逻辑,但需注意线程安全。稳定与易变内容分析:
ElevatorStrategy
接口提供了稳定的扩展点。SCHE
、UPDATE
、换乘的引入,电梯的状态管理和行为决策变得越来越复杂。synchronized
到混合使用 synchronized
、对象锁、ReentrantLock
,同步策略随需求变化。PersonRequest
到增加 ScheRequest
和 UpdateRequest
。双轿厢的同步和防撞是 HW7 的难点。
同步开始改造 (UPDATE
):
Update
类扮演了协调者的角色。当调度器将一个 UpdateRequest
分发给两个相关的电梯后,每个电梯会在其方便的时候(通常是完成当前任务后)调用 update()
方法。update()
方法内部,电梯会调用 Update
对象的 signalReadyAndWait(this)
方法。signalReadyAndWait
方法内部使用 synchronized(updateLock)
保护状态变量 elevatorAReady
和 elevatorBReady
。当一个电梯调用时,它将自己的就绪状态设为 true
。updateLock
上 wait()
。signalReadyAndWait
并设置其状态后,条件 elevatorAReady && elevatorBReady
满足。此时,第二个电梯线程会打印 UPDATE-BEGIN
,执行 sleep(1000)
模拟改造时间,打印 UPDATE-END
,然后调用 updateLock.notifyAll()
唤醒第一个等待的电梯线程。UPDATE-END
之后,它们才各自继续执行后续的清理和状态更新工作。运行时不碰撞:
transferFloor
。两个改造后的电梯,一个服务于 transferFloor
及以上,一个服务于 transferFloor
及以下。它们都可能需要穿越 transferFloor
这个临界点(例如,上面的电梯要去 transferFloor
接人,下面的电梯也要去 transferFloor
接人)。java.util.concurrent.locks.ReentrantLock
类型的 transferLock
来控制对 transferFloor
的访问。这个锁实例在 Update
对象创建时生成,并通过 signalReadyAndWait
方法返回给两个电梯,使得它们共享同一个锁实例。Elevator
的 move()
方法中:transferFloor
时(即 currentFloor + direction == transferFloor
),它会尝试获取 transferLock.lock()
。这会阻塞,直到持有锁的另一个电梯释放锁。isHoldingTransferLock = true
。sleep(speed)
),到达 transferFloor
并打印 ARRIVE
消息。transferFloor
时(即它之前在 transferFloor
,现在移动到 transferFloor + direction
),在 sleep
之后、打印 ARRIVE
消息之前(或者之后,只要保证在让出通道前完成移动即可,代码实现在打印ARRIVE
之后),它会释放 transferLock.unlock()
,并设置 isHoldingTransferLock = false
。transferLock
**,也就保证了只有一个电梯能够正在穿越或停留在 transferFloor
的“通道”上,从而避免了碰撞。多线程程序调试困难是常态,主要问题包括:
Update
的协调机制避免了双方互相等待、transferLock
是单一资源锁)可以规避常见死锁。但如果锁获取顺序不当或嵌套锁过多,仍有风险。我遇到的 Bug (示例性,结合代码可能出现的问题):
SCHE
强制清空乘客和请求队列时,这些请求被重新加入 mainQueue
。如果此时调度器正好判断 mainQueue
为空且输入结束,可能会错误地认为系统可以终止,而实际上还有被重新调度的请求未处理。这需要确保终止条件的判断足够鲁棒,考虑到所有可能的请求来源和状态。TimableOutput
要求输出严格按时间戳递增。但 sleepWithAdjustment
逻辑如果计算不精确,或者线程调度延迟过大,仍可能导致输出时间戳混乱。尤其是在涉及多个线程协作的点(如 UPDATE-BEGIN/END
,乘客进出),需要仔细管理 lastPrintTime
和 sleep
。wait/notify
错误: 在错误的锁对象上调用 wait/notify
,或 notify
后没有再次检查等待条件(Spurious Wakeup)。调试方法:
TimableOutput
/ println
大法: 这是多线程调试最常用的方法。在关键位置(获取/释放锁、状态改变、线程交互点、循环开始/结束)打印带有线程 ID 和时间戳的信息,追踪程序的执行流程和并发行为。线程安全:
synchronized
简单方便,适用于大多数情况;ReentrantLock
提供更强的功能(如尝试获取锁、可中断获取、公平性选项),适用于更复杂的场景;volatile
保证可见性但不保证原子性,适用于简单标志位;Atomic
类提供原子操作。需要根据具体需求选择。wait/notify/notifyAll
: 它们必须在持有锁的同步块内调用,并且 wait
后通常需要循环检查等待条件。层次化设计:
ElevatorStrategy
)定义协约,降低模块间的耦合度,提高灵活性和可扩展性。通过这三次作业,深刻体会到设计良好、线程安全的多线程系统是一项挑战。它不仅要求掌握并发编程的基础知识,更需要细致的分析、周全的设计和严谨的实现。层次化设计是管理复杂性的有效手段,而对线程安全的深刻理解和实践则是保证程序正确运行的关键。