OO第二单元总结
OO第二单元总结
本单元作业模拟了多线程实时电梯系统,实现了多楼层多楼座的横向及纵向电梯调度及乘客换乘请求的实现。本单元作业让我一方面学习了生产者-消费者的架构模式,另一方面深刻体会了多线程编程以及共享数据安全问题
第一次作业
第一次作业实现每个楼座只有一部纵向电梯的实时调度;乘客请求不允许跨楼座且起始楼层与目标楼层不相同
锁与同步块
- 本次作业的锁与同步块均出现在调度器类这个共享对象的方法上,将涉及到对共享对象读写的方法都进行了加锁;现在反思这种设计实际上降低了效率,因为本次作业中虽有多部电梯但各电梯之间并无实际关联,将调度器作为输入线程与所有电梯线程间的共享对象,导致一部电梯抢到锁后进行读写操作时,其它电梯无法抢到共享对象这把锁,而实际上此时其它电梯可以对其楼座的请求队列进行操作;第一次作业由于初次面对多线程问题,更多的参考了往年学长的博客,而自身对架构欠缺了一些思考
- 由于第一次作业对多线程的数据安全问题还是很迷惑,因此仅选择了将方法加锁,最终未出现线程安全问题
调度器设计
- 本次作业将调度器设计为普通的类作为输入线程与电梯线程的共享对象,调度器涵盖每个楼座的一个请求队列;输入线程得到请求后立即将其放入对应楼座的请求队列,电梯线程再从对应楼座的请求队列中获取请求
策略及算法
电梯策略
- 电梯策略采用LOOK算法,即电梯仅接收与其运行方向同向的请求,每到达一层后判断电梯内是否还有请求或当前运行方向上是否还有请求,若有则继续运行,否则转向
- LOOK算法更贴近实际生活中电梯的运行策略,在作业测评中相比ALS算法性能更优
整体架构
classDiagram
class MainClass
MainClass: +main(String[] args)void
class Output
Output: +print(String str)void
class InputThread
InputThread: -Scheduler scheduler
InputThread: +run()void
class Elevator
Elevator: -int name
Elevator: -int moveTime
Elevator: -int openTime
Elevator: -int capacity
Elevator: -Scheduler scheduler
Elevator: -ArrayList<PersonRequest> peopleInside
Elevator: -int currentDirection
Elevator: -int curFloor
Elevator: +run()void
Elevator: +willOpen()boolean
Elevator: +move()void
Elevator: +open()void
Elevator: +close()void
Elevator: +getOff()void
Elevator: +getIn()void
Runnable<|..InputThread
Runnable<|..Elevator
class Schedule
Schedule: -ArrayList<PersonRequest> requestListA
Schedule: -ArrayList<PersonRequest> requestListB
Schedule: -ArrayList<PersonRequest> requestListC
Schedule: -ArrayList<PersonRequest> requestListD
Schedule: -ArrayList<PersonRequest> requestListE
Schedule: -int inputEnd
Schedule: +addRequest(PersonRequest request)void
Schedule: +inputStop()void
Schedule: +isInputEnd(int elevatorName)boolean
Schedule: +peopleWillIn(int name, int floor, int direction)boolean
Schedule: +removeRequest(int waitingFloor, int elevatorName,int direction, int capacity)ArrayList<PersonRequest>
Schedule: +hasRequestThisWay(int elevatorName, int direction, int floor)boolean
Schedule: +changeDirection(int elevatorName, int floor)int
- 本次作业程序结束策略即当输入线程接收到NULL时将共享对象(调度器)的输入结束标志置1,电梯线程在检测到输入结束标志有效且电梯内及电梯队列均无人时结束
bug分析
- 本次作业由于没有保证输出安全导致在互测中被hack若干
- 新增一个将输出包封装好的安全输出类,每次需要打印信息时调用此类的打印方法即可
第二次作业
第二次作业在前一次作业的基础上,新增了横向电梯以及添加电梯的请求,即不同楼座和不同楼层可能有多个电梯;同时新增乘客的环向请求,但只存在起始终止楼座相同或起始终止楼层相同的请求,即乘客不需要换乘
锁与同步块
- 共享对象类中涉及对共享对象读写的方法加锁情况与第一次作业大致相同
- 将新增电梯的请求放到输入线程中实现,因此输入线程中也包含各楼座(层)请求队列这一属性;在新增电梯时输入线程中使用了对象锁
调度器设计
- 本次作业将调度器改为了线程类,同时新增一个总请求队列作为输入线程与调度器线程的共享对象;将各个楼座和各个楼层的请求队列作为调度器线程与电梯线程的共享对象;各楼座(层)的请求队列作为本楼座(层)各个电梯之间的共享对象
- 输入线程得到请求后判断请求类型,若为乘客请求则加入总请求队列;调度器从总请求队列中获取请求并加入此请求对应的楼座(层)请求队列;电梯线程从其对应的楼座(层)请求队列中获取请求并执行
策略及算法
电梯策略
- 纵向电梯策略与上次作业相同;横向电梯策略采用类LOOK算法,将顺逆时针运行类比做电梯的上下行,将当前所在楼座作为中间楼座,根据当前电梯的运行方向将剩余四个楼座划分为相对此楼座的顺向或逆向
- 横向电梯采用类LOOK算法具有相对较好的性能,但容易出现很多细节问题导致bug
- 在电梯类内实现两个构造方式,用于分别实现横纵向电梯
调度算法
- 本次作业涉及各楼座或楼层多部电梯的调度问题。我选择采用自由竞争的调度算法,每次调度器仅将请求加入各楼座(层)的请求队列,由本座(层)的所有电梯自由竞争这些请求
- 自由竞争算法的优点是实现起来比较简单,代码复杂度较低,同时在作业测试中具有较好的性能;但当请求较少时多部电梯会一起冲向同一个请求,造成电梯资源的浪费
整体架构
- 本次作业由于对调度器的修改以及新增了较多功能,实际上进行了一次小的重构,相比第一次作业:
- 新增对已有PersonRequest类的封装类Person,主要补充对乘客请求的一些属性及方法;将调度器Schedule改写为线程类,并增加PersonQueue类作为共享对象(总请求队列及各楼座(层)请求队列)的类别,获取、增加请求等对于共享对象的方法加锁后封装在此类中
classDiagram
class MainClass
MainClass: +main(String[] args)void
class Person
Person: -PersonRequest request
Person: -boolean isTaken
Person: +isTaken
Person: +setTaken
class Output
Output: +print(String str)void
class InputThread
InputThread: -PersonQueue waitQueue
InputThread: -ArrayList<PersonQueue> elevatorQueues
InputThread: +run()void
InputThread: -getPersonQueue(String elevatorType, ElevatorRequest elevatorRequest)PersonQueue
class Elevator
Elevator: -int name
Elevator: -int moveTime
Elevator: -int openTime
Elevator: -int capacity
Elevator: -int type
Elevator: -Scheduler scheduler
Elevator: -ArrayList<PersonRequest> peopleInside
Elevator: -int currentDirection
Elevator: -int curFloor
Elevator: -int elevatorBuilding
Elevator: +run()void
Elevator: +willOpen()boolean
Elevator: +move(int type)void
Elevator: +open()void
Elevator: +close()void
Elevator: +getOff()void
Elevator: +getIn()void
class Schedule
Schedule: -PersonQueue waitQueue
Schedule: -ArrayList<PersonQueue> elevatorQueues
Schedule: +run()void
Runnable<|..InputThread
Runnable<|..Elevator
Runnable<|..Schedule
class PersonQueue
PersonQueue: -ArrayList<PersonRequest> requestList
PersonQueue: -int inputEnd
PersonQueue: -int elevatorFloor
PersonQueue: -char elevatorBuilding
PersonQueue: +addRequest(PersonRequest request)void
PersonQueue: +inputStop()void
PersonQueue: +isInputEnd(int elevatorName)boolean
PersonQueue: +peopleWillIn(int name, int floor, int direction)boolean
PersonQueue: +removeRequest(int waitingFloor, int elevatorName,int direction, int capacity)ArrayList<PersonRequest>
PersonQueue: +hasRequestThisWay(int elevatorName, int direction, int floor)boolean
PersonQueue: +changeDirection(int elevatorName, int floor)int
PersonQueue: +willStop(char building)boolean
- 程序的结束
- 当输入线程接收到NULL时将总请求队列的结束标志置1;当调度器线程检测到总请求队列结束标志有效且总请求队列已空(即已经将所有请求都分配到了对应楼座或楼层的请求队列中)后,调度器线程跳出循环并将所有楼座(层)请求队列的输入结束标志置1;当电梯线程检测到其对应楼座或楼层请求队列的结束标志有效且队列已空后,电梯线程结束
bug分析
- 本次作业由于横向电梯的策略考虑不周,导致有可能出现当前楼层还有请求但横向电梯不能接人并一直环向运行的bug,导致强测个别点及互测出现RTLE,并且这种错误有一定出现几率,即其余正确的测试点也可能隐藏着同样的问题,只是在本次测试中没有复现
- 对横向电梯策略进行了修改,但又导致CTLE,此问题主要是横向电梯在获取请求决定运行方向与电梯是否暂停之间存在矛盾,导致电梯线程轮询
- 在定位到问题并未花费很长时间,但由于对多线程的不熟悉,花费了较长时间才明白出现CTLE的原因,同时修改错误也花费了不少时间
第三次作业
第三次作业在前两次作业的基础上增添了乘客的换乘请求,即乘客请求的起始终止楼座可不同,起始终止楼座也可不同;同时将电梯的运行速度和容量以及横向电梯的可停靠楼座信息改为了可自定义形式
锁与同步块
- 本次作业的锁与同步块与第二次作业区别不大
调度器设计
- 本次作业的调度器设计与第二次作业区别不大
策略及算法
- 电梯策略与调度策略与第二次作业相同
换乘策略
- 对于乘客指令采用能不换乘就不换乘,对于必须要换乘的指令采用静态拆分及仅一次换乘的方法;对于换乘楼层的确定采用了基准策略;在输入线程接收到指令时将此请求的终止楼层楼座保存,在Perosn类中添加目标楼层及目标楼座的属性,并根据基准策略得到换乘楼层作为目标楼层,根据具体情况设定好正确的楼座作为目标楼座;在请求出电梯后判断当前的楼层楼座是否为请求的最终目的地,若不是则更新起始楼层楼座为当前的楼层楼座,并根据具体情况更新正确的目标楼层楼座,将此后半段请求重新加回总请求队列
- 优点是完成起来相对简捷不易出错,但相比动态拆分和实现多次换乘可能性能略低,但从最终强测的结果来看我认为在绝大多数测试点上差别不大
整体架构
- 本次作业的整体架构与第二次区别不大,主要在于涉及到换乘请求的拆分和线程安全问题导致的类中方法的添加与修改
classDiagram
class MainClass
MainClass: +main(String[] args)void
class Person
Person: -PersonRequest request
Person: -boolean isTaken
Person: -int fromFloor
Person: -int toFloor
Person: -char fromBuilding
Person: -char toBuilding
Person: -int destinationFloor
Person: -char destinationBuilding
Person: +isTaken
Person: +setTaken
class Output
Output: +print(String str)void
class InputThread
InputThread: -PersonQueue waitQueue
InputThread: -ArrayList<PersonQueue> elevatorQueues
InputThread: +run()void
InputThread: -getPersonQueue(String elevatorType, ElevatorRequest elevatorRequest)PersonQueue
InputThread: +getTransferFloor(char fromBuilding, char toBuilding, int fromFloor, int toFloor)int
InputThread: +getPerson(PersonRequest personRequest)Person
class Elevator
Elevator: -int name
Elevator: -int moveTime
Elevator: -int openTime
Elevator: -int capacity
Elevator: -int type
Elevator: -Scheduler scheduler
Elevator: -ArrayList<PersonRequest> peopleInside
Elevator: -int currentDirection
Elevator: -int curFloor
Elevator: -int elevatorBuilding
Elevator: -PersonQueue waitQueue
Elevator: -int switchInfo
Elevator: +run()void
Elevator: +willOpen()boolean
Elevator: +move(int type)void
Elevator: +open()void
Elevator: +close()void
Elevator: +getOff()void
Elevator: +getIn()void
class Schedule
Schedule: -PersonQueue waitQueue
Schedule: -ArrayList<PersonQueue> elevatorQueues
Schedule: +run()void
Runnable<|..InputThread
Runnable<|..Elevator
Runnable<|..Schedule
class PersonQueue
PersonQueue: -ArrayList<PersonRequest> requestList
PersonQueue: -int inputEnd
PersonQueue: -int elevatorFloor
PersonQueue: -char elevatorBuilding
PersonQueue: +addRequest(PersonRequest request)void
PersonQueue: +inputStop()void
PersonQueue: +isInputEnd(int elevatorName)boolean
PersonQueue: +peopleWillIn(int name, int floor, int direction)boolean
PersonQueue: +removeRequest(int waitingFloor, int elevatorName,int direction, int capacity)ArrayList<PersonRequest>
PersonQueue: +hasRequestThisWay(int elevatorName, int direction, int floor)boolean
PersonQueue: +changeDirection(int elevatorName, int floor)int
PersonQueue: +willStop(char building)boolean
- 程序的结束
- 本次作业关于程序的结束问题困扰了我很久,具体问题是:由于加入了换乘请求,可能存在输入线程已经接收到NULL指令将总请求队列结束标志置1,调度器检测到总请求队列结束标志有效且总请求队列为空时就会结束,并将电梯线程结束标志置1。但此时可能存在换乘的请求还未添加到总请求队列的情况,导致某些换乘请求不能全部完成
- 于是在讨论课上提出了这个疑问,同组同学给出了可以采用第二次实验课代码中的acquire及release方法解决,最终也采用了此方法可以很简单的解决这个问题
- 正确处理线程的睡眠
- 在前两次作业将线程的睡眠放在了if语句中,这可能使线程被虚假唤醒从而导致轮询
- 将wait语句放于while循环中,在真正满足被唤醒条件时再跳出循环,否则继续睡眠
bug分析
- 在中测时有一个点出现CTLE,花费了很多时间发现是,在我的程序逻辑中,两部同座或同层的电梯在接人及获取请求时存在矛盾,导致一部电梯获取此请求但未进电梯时,另一部电梯仍获取此请求,导致后面这部电梯出现轮询
- 本次程序在强测及互测中的bug依旧存在于横向电梯的RTLE,虽然上一次作业自认为修复了这个问题,但本次作业由于其它策略原因依旧又出现了这个问题,我认为第二次作业横向电梯的完成是这单元最大的问题,而自己又因为不想破坏整体架构以及懒惰没有对其进行重写,而是不断修修补补,导致后续bug不断。
UML协作图
- 由于三次作业的迭代性及涵盖性仅体现出第三次作业的协作图,前两次作业的协作图即包含其中
互测策略
- 本次互测主要采用手动构造边界数据,主要针对自己出现过的或容易出现的线程安全等问题,比如横向电梯的策略问题可能造成的CTLE或RTLE,以及同一时间投放大量请求等问题;但多线程测试具有不确定性,且各人的程序架构有别,相比第一次互测难度要更高
心得体会
- 线程安全
- 对于线程安全的理解随着三次作业一步步加深。第一次作业对其的理解仅限于共享对象加锁,而对整个多线程程序的安全问题还是困惑的并没有完整的认识;随后两次作业可以在完成代码的同时更清晰的明晰每个线程之间的关系,它们对哪些共享对象进行什么样的访问,可能会出现怎样的问题,同时在后两次作业花费了大量的时间去寻找并修改各种CTLE和RTLE的问题的同时也深化了自己对cpu轮询的理解与处理
- 层次设计
- 虽然在作业之初想在第一次作业就搭好具有较好扩展性的架构,但还是在第二单元进行了一小部分的重构,从第二单元到第三单元的架构则没有太大的变动;相比第一单元我认为本单元的整体架构与层次设计还是有进步的,最终呈现的架构也还算清晰,但仍存在部分类耦合度较高的问题
- 回望本单元作业,我认为相比第一单元还是有进步的,一方面比起第一单元勉勉强强的完成,在整体完成度上有所提高,真实的感觉自己学到了很多,另一方面在心态上虽然仍然起伏,但没有之前那么的焦虑;同时在本单元更体会到研讨课的意义,能够听取他人的优点并提出自己的问题大家一起讨论,可以更有效的解决问题;希望在下一单元能有更多收获