其他分享
首页 > 其他分享> > BUAA_OO Unit_2

BUAA_OO Unit_2

作者:互联网

伴随着一路艰辛,oo的第二单元正式告一段落,无论是对作业的回顾,还是自己走过这一单元的心得,正好也就借此博客一书胸臆;

好了,文青的话结束了

总而言之,欢迎大家看我的博客,顺便给几个赞呗

单元回顾

本单元以电梯调度为背景,聚焦于多线程的设计和处理

第一单元

存在A、B、C、D、E五栋楼,每栋楼存在一个纵向电梯,每位乘客将告知其起始位置和目标位置

第二单元

在一单元基础上,增设横向电梯,且可以动态增加电梯,乘客请求分为横向请求和纵向请求,且保证无需换乘

第三单元

在第二单元基础上,电梯容量和运行速度可设置,且横向电梯可设定可停靠位置,存在换乘请求

各次作业分析

本部分将依次分析各次作业,但将主要以第三次作业对整体进行分析

第一次作业

整体思路和作业分析

双线程的选择

正如提示所言,本次作业可以选择三线程和双线程。但我无论是本次作业,还是第二、三次作业,最终都选择了双线程,以下是我的一些思考和设计

线程安全性

线程数过多,尤其是不同种类的线程(即将所有电梯视为同一种线程),不同线程会需要访问不同的共享变量,而在其访问不同共享变量时,不恰当的访问顺序很容易导致死锁等一系列问题。双线程则完全不需要考虑这个问题,所有共享变量本质上可以等价于一个共享变量,设计上更为轻松,运行也更为安全

调度分析

考虑电梯的调度需求,可以发现,电梯只有在其自身状态改变(包括乘客进入和离开)和输入线程状态改变(插入新指令,结束输入线程)时需要更新调度,在其楼层更新时没有必要对调度进行更新,例如电梯中只有一个乘客从1楼到10楼,那么在电梯1楼到10楼的运行中,没有任何必要重新进行调度。而这也恰好契合双线程设计,由电梯线程和输入线程选择进行重新调度

电梯

电梯在我的作业中只是一个工具类,负责sleep相应的时间,完成相应的输出即可,自身无需承担任何的逻辑功能,无需判断乘客是否需要进出、电梯移动方向等一系列问题,甚至电梯也不承担相关信息的保存工作,直接保存在共享变量中。因此,电梯的所有运动操作,完全依赖于共享表量表,换句话说,只需锁住共享变量,那么可以认为电梯的一切状态都处于静止状态。

调度器/共享变量表

为了可扩展性,调度器实际上分为两层架构,section作为父调度器,而orderTable作为子调度器(或者共享变量表),section中保存了未分配的指令,orderTable则是保存了其对应电梯所需的信息,对于本次作业,实质上为一个调度器(每栋楼仅仅只有一台电梯,而所有orderTable的访问均需通过section)因此在下方分析中,视为一个整体。

在我的设计中,调度器是一种“全知全能”的调度器,为什么这么说呢?

  1. 调度器保存了电梯的几乎所有信息(除了电梯ID等这种完全不影响调度策略的信息)

  2. 调度器可以设定电梯的所有信息,调度器会保存每层楼的进出人员表,以及未分配的指令,调度器保证电梯只需访问设定的进出人员表就可完全正常有效的执行,这也与上文电梯中提到了电梯不承担具体的逻辑功能相符合

同时,调度器也保证了在调度时,会同时终止新指令的加入和电梯状态的改变,保证调度从开始到结束,其理想的调度信息来源不变

策略类

策略类作为调度器的一个属性,本身也是为了可扩展性,由于在我的设计中,调度器过于全能,策略类也不可避免的显得略微臃肿。

策略类负责管理未分配指令、已分配但未进入电梯的指令、电梯中的指令,以及各层楼的人员进出、电梯运行方向、电梯是否终止

同步块和锁

锁的选择

本次作业综合使用了ReentrantLock锁和synchronized锁

section

本类中加锁方法已列于下方,功能以注释形式简要标注

section中加锁方法均涉及到了信息获取和修改,因此也必须通过lock保证信息的同步

本类使用ReentrantLock锁,之所以使用非synchronized锁为了提供一定的扩展性,因此选择java提供的lock类,同时由于部分方法内嵌套调用了其他方法,因此使用可重入锁

方法名作用关系分析
setNoNewOrder 设定不存在新指令进入,并进行重新分配 不存在新指令进入将修改信息,并请求重新调度,此时必须通过lock确保信息同步以及电梯状态不变
addOrder 添加新指令,并进行重新分配 新指令进入将修改信息,并请求重新调度,此时必须通过lock确保信息同步以及电梯状态不变
getUndistributed 获取并清空未分配指令列表 此方法主要由策略类调用,由于输入线程和电梯线程都可能使用策略类,此方法实际被两个线程所共享。除保证信息同步外,输入线程调用此方法时,锁将用于停止电梯运行,避免信息修改而策略类无法获取,电梯线程调用时,用于避免新指令等的进入。
isClosed 获取电梯是否处于已经关门且未更新楼层的间隔态 同上
isNoNewOrder 获取电梯中的指令 同上
getProcessing 获取电梯中的指令 同上
getFloor 获取电梯当前楼层 同上
recycleOrders 获取并清空先前分配给电梯,但是并未进入电梯的指令 同上
setUndistributed 设定未分配指令 同上
setWaitingOrders 设定电梯接下来各层将要处理的指令 同上
addLeaveList 设定指定楼层离开指令 同上
addEnterList 设定指定楼层进入指令 同上
setDir 设定电梯运动方向 同上
setFinal 设定电梯结束标志 同上
askForDistribute 调用策略类进行调度 此处虽然不涉及信息的直接获取和修改,但是,为了保证统一性,同时lock为可重入锁,加之涉及调度完成后的线程唤醒,通过lock保证原子性,同时,也避免电梯线程和输入线程同时调用调度方法
askForEnterList 获取进入人员指令 本方法及以下方法为电梯请求信息,然而,由于电梯不承担逻辑功能,逻辑实质由方法自身提供,返回id列表供电梯输出即可。显然,这也涉及到了信息的修改,对于本方法,进入人员id返回时,也将指令从分配但未进入队列移至电梯中指令队列。同时,lock也可保证在无法获取锁,即输入线程调度时,电梯无法更改状态
askForLeaveIdList 获取离开人员指令 同上
aksForPause 询问是否需要停顿 同上
askForUpdateFloor 请求楼层更新 同上
OrderTable

本类与Section类相仿,本身实际保存对应电梯的信息,加锁方法基本同Section,用于保证获取和修改的信息及时同步

方法名作用关系分析
getProcessingOrders 获取电梯中的指令 本类方法大部分与Section同名,其作用实际也相仿,lock关系基本与其相同
getFloor 获取电梯所在楼层  
setWaitingOrders 设定电梯接下来各层将要处理的指令  
addLeaveList 设定指定楼层离开指令  
addEnterList 设定指定楼层进入指令  
setDir 设定方向  
isClosed 获取电梯是否处于已关门和更新楼层间的间隔态  
getDir 获取电梯方向  
askForEnterList 请求指定楼层的进入人员id  
askForLeaveList 请求指定楼层的离开人员id  
aksForPause 询问电梯是否需要停顿  
updateFloor 更新楼层  
setClosed 设定电梯处于关门和更新楼层间的间隔态  
FloorOrderList

本类用于保存每层楼的进入人员和离开人员

由于本类作用简单,因此使用synchronized

本类所用方法均为简单的访问和修改,由于同时可能被输入线程和电梯线程使用,通过加锁保证信息同步

方法名作用关系
clear 清空重置保存的进入人员和离散人员信息  
addLeaveList 添加离开指令  
addEnterList 添加进入指令  
isNeededToPause 询问本楼是否需要停顿  
getLeaveList 获取离开指令id  
getEnterList 获取进入指令id  
Output

本类为对官方输出线程的简单封装,选择使用synchronized锁即可

方法名作用关系
println 输出 需要保证输出方法同时只能被一个线程所调用,避免时间戳异常

架构分析

 

调度器设计

本次作业调度器实际与section共享变量合并,通过类中策略类属性调用策略类进行调度,对外不提供直接的调度方法,只提供相应的功能对应方法,在这些方法中自动进行调度。

第二次作业

整体思路和作业分析

第二次作业虽然添加了横向电梯,但由于并不涉及换乘,本质上其实就是15栋楼,因此整体架构无需大幅更改

当然,由于电梯的种类划分,相应的策略类、调度器等也需进行一定程度的修改,在本次作业中,选择通过继承和实现接口等方式尽可能统一和减少代码量

本次作业主要对如下进行了更改或者优化

  1. 重新归类了各类,定义了相应的软件包;

  2. 修改了部分方法名称,规范统一了命名

  3. 策略类增设一个公共接口,并有两个具体的策略类实现该接口

  4. 电梯类增设一个虚类,并有两个具体的电梯类继承此类

  5. 调度器架构修改

策略类

分析可知,由于横向电梯为环形电梯,因此实际策略与纵向电梯必然存在区别,然而,实际使用时,调度器仅需启动策略类进行调度,而无需关心策略类内部实现,因此,选择Strategy作为接口,对外提供distribute方法,由CircularStrategy(环形电梯策略类)、LineStrategy(纵向电梯策略类)实现此接口即可。

子调度器

子调度器虽然依赖于电梯种类,但实际上大部分属性和方法完全一致,唯一需要修改的方法主要涉及请求更新时的返回值,为了实现架构的统一性,子调度器选择使用虚类继承和泛型的方法进行统一

电梯共享变量

同子调度器相似,选择使用虚类继承和泛型的方法进行统一

架构分析

dir包

 

distributor包

 

elevator包

order包

orderTable包

output包

passenger包

 

schedule包

strategy类

 

unit包

 

同步块和锁

Scheduler

本类实际上基本与第一次作业的Section类相似,因此仅列举部分有所修改的方法

方法名作用关系分析
reset 重置共享变量,在第一次作业中,此作用由部分方法自动实现,本次作业起独立作为一个方法,由策略类刷新共享变量时使用 此处涉及了大量信息的修改,必须由锁进行同步控制
askUpdate 实际为首次作业中askForUpdateFloor的泛化版本,可实现楼层或者楼座的更新  

ElevatorOrderTable

本类虽然作用与首次作业的OrderTable相似,但本次作业选取了更加高效的读写锁进行同步控制

方法名作用关系分析
getProcessingOrders 获取电梯中的指令  
getFloor 获取电梯所在楼层 本方法与getSection方法均为虚方法,具体由子类实现,对于纵向电梯而言,楼层可变,使用readLock保证获得信息为实时信息,楼座不可变即无需加锁,横向电梯恰好相反
getSection 获取电梯所在楼座  
getDir 获取电梯方向 readLock控制,保证读取信息为最新信息,且读取时不会修改
setDir 设定电梯方向 writeLock控制,本处将修改电梯信息,使用writeLock避免其余线程同时进行读取和修改
askProcessingOrders 获取电梯中的指令 本处仅涉及信息的获取,选择readLock进行控制
reset 重置各层人员进出列表 选择writeLock进行控制,避免其余线程的访问和修改
getWaitingOrders 获取并清空电梯中等候指令 同上
setWaitingOrders 设定电梯中等候指令 同上
addLeaveList 设定指定楼层离开指令 同上
addEnterList 设定指定楼层进入指令 同上
getEnterList 获取当前楼层进入指令id 尽管为获取,然而由于方法中伴随有对指令状态的修改,因此选择writeLock进行控制
getLeaveList 获取当前楼层离去id 同上
askPause 询问是否需要停顿 读取共享变量中信息,readLock保证同时不被修改即可
update 更新楼层/楼座 本方法实际为虚方法,具体由子类实现,涉及有信息的修改,选择writeLock进行控制
close 将电梯更新为处于已经关门和未更新电梯楼层的中间状态 涉及了对信息的修改,选择writeLock进行控制
open 取消close状态 同上
isClosed 询问是否处于close状态 询问信息,使用readLock保证期间信息不被修改即可

UnitOrderTable

完全等价于第一次作业的FloorOrderList

调度器设计

本次作业选用两级调度器控制结构,父调度器负责指令分发给指定电梯的子调度器,子调度器负责相应电梯的控制

交互方面仍然与首次作业相同,在输入线程和电梯线程更新信息时自动调用调度方法

第三次作业

架构分析

线程架构

本单元作业均选取双线程模式,即一个输入线程以及n个电梯线程,其余的所有类和方法均由这些线程所调用

优点

此架构最大程度的减少了线程数量以及共享变量的数量,也避免了诸如生产者消费者关系反复嵌套的麻烦

缺点

可能一定程度上影响性能,例如分配指令时将锁住对应楼座或者楼层全部的电梯,但是本次作业并非高并发场景,同时运行的线程数明显小于100,线程切换,锁的切换影响并不明显,因此牺牲可能的性能问题换取更高的安全性也是值得的

 

 

调度架构

本次作业调度架构共分为三层,各层之间处于递进关系

Spliter

分配器,负责输入请求的拆分以及分配,同时负责控制请求是否最终完全完成

Section/Floor

具体至每一栋楼或每一层的调度器,负责将请求分配给指定的电梯调度器

Scheduler

负责电梯的调度运行,同时也负责反馈Spliter其执行情况

整体架构

本次作业共分为9个软件包,27个类

distributer软件包

本软件包包含了Input类和Spliter类,由于输入线程实际上也涉及了一部分请求分发(将请求分类为人员请求和电梯请求),因此并入此软件包

 

Input类

本类承担输入线程的工作,也是唯二的实现Runnable接口的类之一,Input类主要负责及时获取输入的指令,并对其进行初步的分类(分为人员添加指令和电梯添加指令),调用Splitter的相应方法,也负责在输入线程结束后通知Splitter

Splitter

本类承担指令分配器的功能。

  1. 负责对所有楼层和楼座的总体管理;

  2. 负责对指令的进一步解析和分配。

  3. 负责对于乘客的管理,包括维护乘客指令的执行情况和对乘客指令的分解

  4. 负责初始化楼座和楼层

elevator软件包

本软件包包含了本次作业中的电梯,以Elevator类作为虚类,CrosswiseElevator、VerticalElevator继承该类并实现相应的方法

 

Elevator

本类为虚类,提取了电梯的公有属性和方法,为了提高扩展性,使用泛型。

  1. 保存了电梯的一些静态属性(如容量、id等),并提供方法进行访问。

  2. 实现了Runnable接口,作为线程依次模拟电梯的各个状态和输出相关信息

CrosswiseElevator

横向电梯,继承了Elevator类

VerticalElevator

纵向电梯,继承了Elevator类

order软件包

在本次作业中,每个乘客可视为多个指令的复合体,因此,本软件包包括了Order类、Passenger类以及Order的辅助类OrderType

 

Passenger

本类保存了乘客的相应信息

  1. 负责对乘客起始位置、终止位置以及当前位置的保存

  2. 对外提供方法访问相关信息,并可返回乘客是否已经到达目的地

  3. 可直接解析PersonRequest

Order

本类为电梯子调度器实际处理的指令

  1. 保存了指令的部分信息,特别的保存了最终位置的信息,便于及时进行动态调度

  2. 为每个指令提供了独一无二的id,id唯一依靠于指令的产生时间

  3. 实现了CompareTo接口

  4. 对外提供了多种构造方法,便于指令动态拆分时的修改(新指令尽管与旧指令不同,但其Id和最终位置应当相同)

  5. 提供方法判断是否可以进行动态调度

OrderType

枚举类,标志Order为横向指令抑或是纵向指令

orderTable软件包

 

ElevatorOrderTable类

本类作为电梯信息保存的共享变量,设计为泛型虚类

CrosswiseElevatorOrderTable

横向电梯共享变量,实现了ElevatorOrderTable类

VerticalElevatorOrderTable类

纵向电梯共享变量,实现了ElevatorOrderTable类

UnitOrderTable类

本类保存了每层楼的人员进出列表

Output软件包

 

Output类

本类作为输出类,本质上是对官方输出线程的再次封装,除提供对String直接的输出方法外,也提供format化的arrive、open等输出方法

path类

本软件包涉及对各种距离、路径处理的工具类

 

Accessible类

本类主要负责维护各个楼层的可达性情况

Dir类

本类为枚举类,负责对电梯运行方向的记录

Location

本类保存位置,含楼座和楼层

Path

本类为大量静态方法的集合

  1. 针对Dir提供统一的位置索引(如下一层/座)

  2. 提供统一的距离获得(包括指定方向、不指定方向、中转楼层等等)

  3. 提供统一的方向判断

  4. 提供可达性识别

scheduler软件包

本软件包为具体电梯的调度器

 

Scheduler类

调度器的父类,虚类,使用泛型

CrosswiseScheduler

横向调度器,继承了Scheduler类

VerticalScheduler

纵向调度器,继承了Scheduler类

strategy软件包

本软件包保存了电梯的具体调度策略类

 

Strategy

公共接口

CrosswiseStrategy

横向调度策略,实现了Strategy接口

VerticalStrategy

纵向调度策略,实现了Strategy接口

unit软件包

本软件包实质为个楼座和各楼层的调度器

 

Floor

楼层的总调度器

FloorAccessible

本类为Floor的辅助类,帮助维护一层中所有电梯的可达性

Section

楼座的总调度器

复杂度分析

ClassOCavgOCmaxWMC
Launch 1 1 1
distirbuter.Input 3 5 6
distirbuter.Splitter 2.27 8 34
elevator.CrosswiseElevator 1.67 3 5
elevator.Elevator 1.29 4 22
elevator.VerticalElevator 1.67 3 5
order.Order 1.32 4 25
order.OrderType n/a n/a 0
order.Passenger 1 1 12
ordertable.CrosswiseElevatorOrderTable 1.43 4 10
ordertable.ElevatorOrderTable 1.28 2 23
ordertable.UnitOrderTable 1.22 3 11
ordertable.VerticalElevatorOrderTable 1.43 4 10
output.Output 1 1 7
path.Accessible 4.33 11 13
path.Dir n/a n/a 0
path.Location 1.33 3 8
path.Path 1.69 4 27
scheduler.CrosswiseScheduler 1 1 2
scheduler.Scheduler 1.12 3 37
scheduler.VerticalScheduler 1 1 2
strategy.CrosswiseStrategy 4.73 16 52
strategy.CrosswiseStrategy.OrderDisCmp 1.5 2 3
strategy.VerticalStrategy 5.07 15 71
strategy.VerticalStrategy.OrderDisCmp 1.5 2 3
strategy.VerticalStrategy.OrderDisMainOrderCmp 1.5 2 3
unit.Floor 1.2 2 6
unit.FloorAccessible 2.67 4 8
unit.FloorAccessible.UnitAccessible 1.67 3 5
unit.Section 1.67 3 10

作业复杂度主要集中在Strategy类,这也是符合预期的,策略类进行决策时需要获取大量信息且也会更新大量信息,其中需要大量逻辑运算

同步块和锁

本次作业综合使用了synchronized锁,ReentrantLock锁以及ReentrantReadWriteLock锁

锁的使用主要分布在分配器(Splitter),总调度器(Floor/Section),子调度器(Scheduler),子调度器中的共享表(OrderTable)以及共享表中的共享对象(UnitOrderTable)以及输出安全类(Output)

Splitter

由于本类可能同时被输入线程和电梯线程使用,且电梯加入、指令加入、分配,乘客信息维护等必须保证信息的可见性以及避免信息重复更新导致相互覆盖,本类选择synchronized锁。

本类的加锁原因主要分为两种:

  1. 对于addSection/addFloor,该方法实际只会被主线程调用,但由于多个线程将通过Splitter访问楼层或楼座的总调度器,需要通过加锁保证楼层调度器和楼座调度器的信息对所有线程可见

  2. 对于其他方法,由于Splitter负责指令的分发和乘客信息的维护,当任意一个进程使用Spliiter时,需要通过加锁保证其余进程无法进入,否则,多个进程同时访问将导致信息的相互覆盖或者获取的信息并非最新值,导致重复分发

Floor/Section

Floor/Section同样可被输入线程和电梯线程访问,本类加锁也同样是为了保证其中电梯数量的可见性以及避免重复执行

Scheduler

本类均维护了部分电梯的信息以及指令队列等,显然必须通过加锁避免修改和访问同时进行

OrderTable

本类同样涉及也只进行电梯信息的保存和指令队列的维护,相较于Scheduler类功能更为单一,选择ReentrantReadWriteLock锁适当提高效率,对于单纯的信息获取方法使用readLock,避免读取时信息修改,对于信息修改方法使用writeLock避免同时的修改和读取

UnitOrderTable

本类仅维护每层楼的人员进出序列,结构功能简单且单一,使用synchronized锁保证不能同时修改和访问即可

可扩展性分析

本作业可扩展性应当较高,其可体现在如下几个方面:

  1. 策略类作为调度器的一个属性,同时本身作为一个接口仅提供distribute接口,对于不同电梯完全可以选取不同策略,在本次作业中,环形电梯选择环形als算法,纵向电梯选择look算法

  2. 调度器极大的权限,本作业中调度器拥有极大的权限,可以自由设置电梯一切指令进出、运行方向等,且高层的调度器可以收取子调度器的所有指令进行再次分配,即实际上可以在每次在每次指令添加和完成时重新设置全局的所有调度

  3. 虚类、泛型的配合使用,一定程度上摆脱了重写方法返回值的限制

自我程序Bug分析

本单元的每次作业均搭建了相应的评测机,而在每次公测提交前,均已经使用评测机运行了1~2天,故公测和互测均未发现Bug,本部分选择分析自我评测时的Bug

第一次作业

电梯未能成功停止,实质上为策略类的终止条件判断失误

第二次作业

电梯重复添加,在初始化时电梯重复添加了两次

第三次作业

本次作业Bug相对较多

  1. Path类中getDis方法错误,对于目标楼层或楼座与当前楼层或楼座相同时,应当返回0,然而误返回了负值或5

  2. Path类中getDir方法错误,对于横向电梯返回了相反的方向,最终导致电梯反复摇摆

  3. 指令完成返回时间问题,在未输出乘客out时,即通知分配器已完成,可能导致乘客先in再out

互测策略

本单元作业主要选取强随机数据轰炸判断是否存在Bug,分析程序找出Bug位置定向构造的方式,实质上与第一单元并无明显不同

互测的有效性其实主要集中在数据生成策略

数据生成策略

以第三次作业为例

  1. 评测机提供多个参数可供设置,便于发现Bug后集中生成符合互测要求且强度更高的数据

  2. 优先生成时刻相同的指令、其次生成0.2s、0.4s关键时刻的指令,其余时刻小概率生成

  3. 优先生成起始位置或目标位置相同的指令

  4. 更大概率生成换乘指令

  5. 更大概率身材隔行容量最小、运行速度最慢、可停靠点最少的电梯

第一次作业

本次作业能明显发现房内部分同学存在输出线程不安全的问题,也是唯一的基本没有使用自动评测的进行hack的作业,hack数据构造分为两个方向

  1. 小概率多次触发

    即仅同时存在几个乘客在不同电梯的进出,但反复触发多次

  2. 大概率单次触发

    规划时间,构造同时60名乘客进出的极限情况

第二次作业

本次作业通过评测机发现了对方的Bug,分析代码可知,一名同学采取自由竞争,在多台横向电梯时可能导致无法停止,另一名同学采取自由竞争,并试图保证Sleep标准的时间,在多台纵向电梯时可能导致close之后瞬间arrive,因此,直接更改评测机参数,分别进行横向指令和纵向指令的集中轰炸

第三次作业

策略同第二次作业

心得体会

本单元相比于第一单元无疑是多了很多的不确定性,由于多线程的不可控,甚至可能单元结束之后代码中仍然存在一定数量的bug,而且多线程下无论是de自己的bug抑或是别人的bug都更为困难,更可能出现各种神奇的玄学情况,例如在自我评测时异常停止,却无法复现,这时候就会极为纠结究竟是程序有问题还是电脑卡住、磁盘空间不足等等问题,对心态也是一种极大的挑战。

与一单元相同的,我的代码量也是显著的高于其他同学,在第三次作业后有效行数到达了惊人的2100+行,其实这也与自己的代码风格有关,我习惯性会对有相关联系的属性归类至一个新的类中,相应的就需要配置大量访问方法,同时,我选取的为双线程模式,且调度器拥有极大的权限,这也导致需要为调度器配置大量的修改、访问方法以实现权限。

总体而言,二单元不愧是OO最为出名的一个单元,其难度也算是名副其实。但是,尽管这导致了我有着痛苦的连续几天debug到天亮的经历,我也清楚的认识到这极大的提高了我的代码能力,尤其是加深了我对多线程的理解。

标签:OO,作业,BUAA,调度,电梯,指令,线程,楼层,Unit
来源: https://www.cnblogs.com/hxyeverywhere/p/16217458.html