OO第二单元博客
第二单元 多线程电梯 学习总结报告一、三次作业同步块设置、锁的选择及锁与代码逻辑的关系回顾本单元三次电梯迭代作业我对于锁的概念与使用、临界区保护、线程共享和资源竞争有了完整的实践理解。在整体代码结构中我主要采用对象锁的方式进行并发控制。电梯Elevator类内部全部使用synchronized (this)作为锁。电梯自身的乘客列表、等待请求队列、楼层信息、运行方向、载重数值、电梯运行状态枚举等都是多个线程会并发访问的共享数据。无论是添加外部乘客请求addRequest、接收维修/更新/回收指令还是电梯内部上下客、楼层移动、状态切换全部包裹在同步代码块中。这样设计的原因是每一台电梯都是独立运行的子线程只需要锁住自身实例就能做到细粒度加锁不会出现全局大锁导致线程阻塞等待的问题。这里值得留意的一点是在对于电梯状态进行快照时的读方法不能进行加锁否则会严重影响性能。调度器Dispatcher中针对静态共享集合加锁。全局电梯列表elevators、等待请求缓冲队列waitQueue属于全局共享资源会被输入线程、多个电梯线程同时访问修改。因此在请求分发、等待队列转移、电梯遍历唤醒等操作时都使用集合对象作为锁保证多线程修改队列时不会出现数据覆盖、请求丢失、集合并发报错等问题。当然这里直接使用 Java 中自带的线程安全容器也是可行的。锁与同步块内语句的关联同步块中只存放必须保证原子性的操作例如状态修改、队列增删、数值读写、条件判断修改组合逻辑。而像耗时的Thread.sleep、单纯的逻辑判断、循环遍历不进行写操作等内容尽量放在同步块外面。一方面可以减少锁的持有时间提高整体并发效率另一方面可以避免长时间独占锁造成其他线程大范围阻塞。同时本次作业大量使用wait()和notifyAll()等待唤醒机制而这两个方法必须写在同步块内部。电梯在无任务时会在同步块内循环等待当调度器分配新请求、或者电梯由Normal状态转变为Double状态从而要在换乘层进行避让时就必须唤醒所有等待线程让电梯重新检测任务并继续运行这样既避免空循环浪费资源也保证多线程之间能够正常地协同工作。二、三次作业调度器设计、线程交互与调度策略分析1. 调度器整体设计思路三次作业我全程采用中心化调度器模式由Dispatcher统一管控全部电梯与全部请求。在程序启动时就静态初始化 6 台主电梯与 6 台备用电梯逐个创建独立线程并启动。对于所有外部请求包括普通乘客请求、电梯维修请求、参数更新请求、电梯回收请求全部先交给调度器统一分发处理电梯本身只负责执行运行逻辑不参与请求选择做到调度与执行分离结构更加清晰。2. 调度器与各个线程的交互方式输入线程InputThread持续读取控制台输入识别不同类型请求调用调度器静态分发方法将请求上交调度器处理。输入结束后修改全局标记位并唤醒所有电梯以便电梯线程能够正常结束。调度器Dispatcher作为中间管理层接收输入线程的请求根据每台电梯当前状态、载重、位置、运行方向筛选可用电梯进而选出最优电梯并添加请求当电梯因超载、维修等原因退回请求时调度器负责二次缓存与重新分发同时调度器也参与全局维护剩余请求计数用来判断是否所有请求已全部处理从而判断电梯线程是否应当结束。电梯Elevators持续循环运行被调度器分配任务后执行接人、送客、移动楼层遇到维修、更新、回收指令时主动切换状态清空当前任务并回退请求完成单次任务后主动等待被唤醒后再次判断是否存在新任务。3. 调度策略设计与多性能指标适配本次作业中按照多个指标进行调度分配具有一定启发式性质由于具体权重参数难以确定可能可以通过大量数据对参数进行机器学习但太麻烦了就没有设计具体的代价函数。筛选优先级依次为首先过滤处于不可接收状态的电梯例如正在维修、单双梯模式限制楼层区间的电梯其次优先排除会直接超载的电梯避免了无效的载客替换优先选择当前静止无任务的空闲电梯减少乘客等待时间优先选择运行方向与乘客出行方向一致的顺路电梯减少电梯绕路最后结合电梯当前总负载、距离乘客发起楼层的远近进行综合排序。这套策略可以同时适配多项要求无论在性能还是拓展性方面的表现都较好而且不算复杂我对此较为满意。三、程序运行出现的Bug 及多线程调试方法总结1. 开发过程中遇到的典型bug1多线程数据竞争问题初期设计时因为疏忽没有给个别任务和状态变量加锁多个电梯同时读取修改共享集合偶尔出现乘客请求莫名消失、重复加载同一乘客、电梯列表遍历异常的问题。后期给所有共享资源增加同步保护后该问题完全解决。2线程无法正常结束曾经出现所有任务执行完毕但电梯线程一直卡在wait()无法退出或在请求返回重新分配前就过早结束的问题。原因是对于结束的判断不完善后通过对请求计数待处理和已处理的方法完善了全局结束条件、在其置 0 后统一唤醒全部电梯修复了上述问题。3双电梯换乘楼层死锁在换乘楼层电梯卡死。初始的逻辑是让电梯在Double状态时若在换乘层无任务停靠便自动向临层避让。后来发现其若在Normal状态抵达换乘层停靠则后续切换至Double状态时必须将其唤醒从而解决了死锁问题。4电梯超载问题未在同步块内统计实时载重上下客并发修改重量导致超重判断失效出现超重依旧运行的问题。2. 针对多线程程序的debug方法大量打印关键日志利用题目提供的TimableOutput在电梯到达、开关门、接收请求、状态切换、等待唤醒等关键位置输出信息直观观察每个线程的运行时序和可能出现bug的节点。缩小问题范围遇到并发错误时针对某一特定的特殊场景设计测试用例逐一检查各功能是否存在问题从而减小每次排查的范围。检查所有共享变量养成习惯只要是多个线程都会访问的变量全部检查是否加锁保护从根源避免线程不安全。四、结合三次作业谈谈对线程安全与层次化设计的理解1. 对线程安全的理解经过三次迭代开发我意识到多线程开发最难的地方在于资源竞争与执行时序不可控。线程安全的核心就是多个线程同时操作共享资源时必须通过加锁、限制访问顺序、保证操作原子性来避免数据错乱。所有临界资源都需要主动保护并且想清楚其中的逻辑否则程序就会在不经意之间随机出现各种bug这在现实的工程中是不可接受的且很难调试。同时也要合理控制锁的范围一味地使用大锁虽然安全但是会严重降低并发效率细粒度锁设计十分重要。另外灵活使用多种类型的锁以及线程协作的工具是多线程高效运行的关键能够让多线程按需高效无误地协同运作。2. 对层次化、模块化设计的理解本次代码的分层较为明确输入层InputThread只负责读入数据不处理业务调度层Dispatcher专注请求分配、全局管理执行层Elevator只负责单台电梯运动逻辑工具层FloorUtil、Reclaim提供通用静态方法和业务线程解耦。而在每个层次中尤其是Elevator进一步将不同的执行策略设计成单独的模块。分层设计的优势非常明显每个类职责单一代码可读性高后期迭代新功能时比如电梯的检修、更新和回收只需要在对应层增加代码例如为状态机增加新状态和相应的运行策略不会大范围改动原有逻辑并且在保证原有部分正确的前提下出现bug时可以快速定位模块极大降低多线程程序的调试难度。因此良好的层次化和模块化设计是复杂多线程项目能够稳定迭代的关键。五、大模型使用心得整体项目架构和核心调度策略全部由我自己设计确定主要使用大模型查阅多线程语法并学习锁的使用方法。对我而言依赖大模型范围地生成代码显然是不理智且不太可行的行为我们还是应当时刻保持自己的思考并借助大模型来提升自己学习新知识的效率再在自己的实践中切实巩固所学。六、第二单元学习真实体验与课程建议1. 单元学习体验相比第一单元第二单元对于多线程的学习其实在程序逻辑的复杂度上并没有提高但主要难点在于接触了全新的知识并且更加强调代码编写的严谨性。我从最开始只会简单创建线程、不理解锁的意义到一步步学习临界区、等待唤醒、死锁避免、分层调度三次作业迭代下来收获很大。同时因为运行结果不确定、bug难以复现多线程代码的调试难度远高于单线程整个开发过程需要耐心和严谨的逻辑思维。但相对应的我也在其中学到了很多成长了不少。无论如何还是恭喜自己顺利熬过了这艰难的一个月2. 课程建议首先是编写代码的时间略显不足希望可以适当延长时间期限其次是希望弱测中测能够公开全部样例或者至少公布典型样例为防止特判骗分可以让每个数据点执行特定的测试功能然后生成大量的测试用例在测试时随机选取这样避免了debug时没有头绪浪费大量时间。