BUAA-OO-2023-第二单元

发布时间 2023-04-16 00:18:02作者: 柳浩东110111

第二单元的主要作业是完成一个多线程电梯调度任务,并学习锁的使用,代码同步和资源共享。理解JAVA中的多线程机制和信号量机制。三次作业的类图基本上没有变化,都是在原有的基础上增加了一些方法和变量,因此直接给出最后一次的UML类图

 

 

以下是线程之间的协作

 

 

同步块的设置和锁的选择


三次作业中,我的Request都是分成一个大的Request和每个电梯自己的Request。每个人的请求产生后先进入大的公共Request,即PersonWaitLine中,它是一个线程,会不断的尝试分配其中的Person,在没有Person可以分配的时候就会阻塞,每次加入Person都会尝试唤醒它。然后把Person给RunningElevator类,这个类会根据运行中的电梯的可达性的集合计算出一个最短路径,把请求优先分配给满足最短路径的电梯中性能最好的一台。这里会访问电梯内部的RequestTable,电梯下一步的运行则是通过电梯向Control类请求下一步操作来得到的,计算下一步的策略也需要用到电梯内部的RequestTable,因此我对所有对RequestTable的访问和修改加上了同步。后来由于有了MainTain,电梯可能向PersonWaitLine中加入Person,因此我也对PersonWatilLine的修改加上了同步。电梯相当于同时扮演了生产者和消费者。

第三次作业中,处理对同一楼层的最大开门限制和只进人电梯的限制,都采用了JAVA自带的信号量机制。对每个楼层都设置一个信号量,初值为最大数,每个电梯开门前尝试获取,关门后释放。

除了基本的sychornize锁和semaphore信号量外,三次作业中没有使用其他类型的同步锁。主要是因为每个电梯的RequestTable都是自身的,会读取它的只有分配线程和它自己,不太需要读写锁之类的锁。

调度策略的设计

本次作业中,使用了适应度的方法来调度电梯,首先不能到达对应楼层的电梯的适应度为最低值。然后,对于那些能够完成请求的电梯,适应度和它的速度成正比,和它的剩余容量成反比,在前二者相同的情况下,为了减少低容量电梯的使用率(低容量电梯移动的耗电和高容量相同,但是移动过程中能够捎带的人更少),又设置了和最大容量成正比。对于第三次作业的换乘要求,具体来说,每个Person的路径从开始的只有起始和结束楼层改为了一个数组,包含了一组起始和结束楼层,这组楼层通过迪杰斯特拉算法得到的最短换乘数量路径得到。每次的电梯运行完成一个请求,然后把人放回PersonWaitLine中,PersonWaitLine在重新分配该人时会检查它的数组元素个数,若为0,说明他已经到了,则不再进入后续分配。RunningElevator在尝试分配一个Person时会检查当前的路径段是否有电梯能够到达,若没有,说明原来的电梯已经被Maintain了,则会根据现有的电梯重新计算路径数组。

迭代过程中的易变和稳定性分析

 

在迭代中,Control类基本上没有什么变化,就是在处理Maintain的时候,就加入一个对电梯isMaintain变量的判断。

发生大变化的是Person类,因为一开始的时候,我没有自己设置一个Person类,而是直接把PersonRequest当作元素来进行操作,在第三次作业要进行分段运输的时候,PersonRequest就不足以完成了,为此只好新建了一个Person类,存储一个PersonRequest和一个路径数组,原来所有PersonRequest的地方都要改成Person,这给我带了的不小的工作量。

此外,在电梯类的迭代中添加了许多的方法,主要是为了判断只进人和可达性的,感觉都是必要的。

剩余的部分,基本上就是由于修改的可能性增加,导致的一些代码加上了同步。还有第三次作业新加上的信号量数组和计算最短路径使用的迪杰斯特拉算法。

三次作业的互测和BUG方面

 

第一次作业中强测没有出现BUG。但是在互测中被找出了一个BUG,就是在同时有大批量的人进入电梯的时候,出现了一个CurrentModificationException。原因在于遍历电梯里的RequestTable时,在电梯开门进人,对RequestTable用迭代器遍历并且删除元素的时候,没有对它进行加锁,原本在单线程的时候,只要使用iterator.remove就能避免这个异常,但是在多线程的时候就不行了。因为在单线程的时候,访问和修改是不会同时进行的,在多线程时,大批量的人同时进入,导致PersonWaitLine分配给该电梯RequestTable和电梯开门时遍历RequestTable同时发生,这里的同时不是真正意义上的同时,而是一个时间段,即在遍历的整个过程中发生了修改,这样也会导致CurrentModificationException。

第二次作业中出现了一个IndexOutOfBoundsException,在尝试分配Person给电梯的时候,出现了只有三个电梯,却分配给了第四个电梯的情况,这里是因为对Elevators数组的增加和删除没有加锁导致的,加上锁后就修复了。

第三次作业没有出现BUG

由于这两个BUG都直接的给出了出错的位置,因此没有尝试太多的针对多线程模式的Debug的方法。

三次作业中,只有第二次作业成功找到了别人的错误,就是在多次连续Maintain的时候,会发生人员丢失的情况。

心得体会

 

这三次的作业中,线程安全一直是一个重要且核心的问题,第一次第二次作业的BUG都出在了线程共享资源的互斥使用上,而且都是难以复现的随机BUG,这也是多线程的一大特点。写的时候的几秒钟的疏忽,Debug的时候就可能花上几个小时。因此我感觉,面对多线程程序,最好的方法就是写的时候就严格考虑到该变量的潜在使用者,分析是否有可能发生死锁或修改和访问同时发生。

关于层次化设计,这次的作业感觉自己在层次化方面做的还行。Inputhandler负责处理三种Request,所有的Person通过统一的PersonWaitLine来加入电梯,所有的电梯由RunningElevators来统一管理,每个电梯有自己的RequestTable不用担心两个电梯之间的影响,电梯的运行由Control类给出下一步的指令。各个类基本上做到了各司其职。不太满意的一些地方就是把最短路径的计算放入了RunningElevator,这一部分感觉可以单独开一个类来维护一张图。