面向对象第二章总结
1. 作业中的同步操作
第二章主题是多线程设计,因此作业的训练主要也是围绕多线程并发问题。我在这三次作业中使用的同步方法有synchronized, lock和readwritelock。
第一次作业中,由于需求简单,各个电梯任务独立,因此涉及的对象贡献资源只有每个电梯自己的候乘表:输入线程往其中加入乘客,电梯从中取出乘客。由于需求简单且刚刚学习多线程设计,我在第一次作业中就简简单单使用了synchronized代码块,在输入线程加入乘客和电梯取出乘客前将候乘ibao这一共享资源锁住,从而实现了同步控制。
第二次作业加入了环形电梯,但乘客的路线只能直线,即不存在换乘,因此本质上而言环形电梯与楼层电梯没有区别。此外,这次作业还加入了同任务多电梯功能,这便引入了更多对象使用共享资源,相应的对同步方法要求也有所提升。考虑到这次可能会有多个电梯查看同一候乘表,倘若一味在访问前对候乘表上锁便降低了读读操作的性能。基于这一考虑,我在这次作业中尝试使用了ReadWriteLock并结合Condition对象实现wait、notify机制。但是我在实现时为了方便,在候乘表类中加入读写锁这一对象,并且将每一个方法提供为同步方法,即针对不同性质的方法,在方法内部使用readLock或者writeLock。这样看似每个方法是同步的,不会产生线程安全问题,但在实际使用中保证每个方法是线程安全的并不足够,有时候需要确保多个方法的使用是原子的。比如,电梯中没有乘客时,首先要看候乘表是否为空,若不为空则搜索乘客的位置,为空则wait。这种场景下观察候乘表是否为空和确定运行位置便是原子的。倘若电梯发现候乘表不为空,准备开始搜索乘客位置前该乘客被另一电梯接走,前者便会陷入死循环的尴尬处境。经过我的反思,最好的办法还是让共享对象类提供上锁的接口,让调用者自己决定如何上锁,是读锁还是写锁。
第三次作业从同步角度来看,引入的一个变化便是电梯轨迹可自定义。从常规思路来看,每一层一个候乘表,电梯对候乘表的共享可能会加强。但我的设计是为每一种运行轨迹的电梯分配一个候乘表,这样的设计可以保证资源的共享强度与上次作业相同,因此电梯这块的同步不需要改变。但是,经过上次作业对多个方法上锁的考虑,我感觉使用读写锁没有意义,因为读完一定会写,因而读的操作附属于写,所有的同步操作都要使用写锁。因此,我在这次作业中改用了普通的Lock接口。
2. 作业中的调度器设计
由于我的策略主要是自由竞争,因而所谓的调度器也并非一个严格意义的调度分配算法,而是一个完成投放请求功能的对象。
第一次作业每个电梯独立拥有自己的候乘表,因此没有什么调度可言,将输入请求直接加入对应电梯候乘表中即可。
第二次作业加入多电梯后便涉及多个电梯职能分配的问题。有的同学仍选择每个电梯拥有自己独立的候乘表,通过调度器合理地将乘客分配给不同的电梯。我个人认为选择合适的调度算法相当困难,所以还是采取了自由竞争的方式,让相同职能的电梯共享候乘表,自己取获取需求。
第三次稍微有点调度器的意思,因为在我的设计中乘客的路线是需要计算最优路径的,因而需要根据不同的阶段将乘客放入对应的候乘表。在我的设计中,我首先将整栋楼转化为图,以电梯的运行路径加入带权边,进而在每个乘客到来时使用深度优先搜索找到耗时最短的路径,以此作为该乘客的实际路径(每加入一个电梯需要重新计算每个乘客的路径)。因此,类似流水线模式,在每个阶段前,我会将乘客放入调度器,由其将乘客投入对应的候乘表。
3. 自己作业架构
第一次作业
第一次作业功能比较单一,每个电梯独立完成自己的任务,因而整体架构比较容易。值得一提的是最开始我对各种电梯调度算法不太清晰,因而使用策略接口来完成具体实现中不同策略的切换,同时每个 策略对应使用的数据结构也不一样,故数据结构也用接口来管理。我在最开始使用的是ALS算法,后来改成了LOOK。但由于我对接口使用不太熟练,导致使用ALS与LOOK对电梯提供的方法不完全一样,因此这个接口使用也不太成功,后来干脆把ALS删了。
类图
classDiagram
class Main {
+static main()
}
class Elevator {
-list:VisitorList
-strategy:Strategy
-id:int
-building:char
-map:Structure
-size:int
-direction:int
+run()
+moving()
+closeDoor()
+getIn(visitors:HashSet~Visitor~, flag:boolean)
+getOut(visitors:HashSet~Visitor~)
}
class Input {
+run()
}
class Output {
+static println(s:string)
+static printElevator()
+static printVisitor
}
class Strategy {
<<interface>>
gotoFloor(elevator:Structure,list:VisitorList, nowFloor:int, direction:int, size:int)
}
class Look {
-safe(floor:int)
+gotoFloor()
}
class Visitor {
-id:int
-startFloor:int
-endFloor:int
-startBuilding:char
-endBuilding:char
-arriveTime:int
}
class VisitorList {
-list:Structure
-finished:boolean
+synchronized add(visitor:Visitor)
+synchronized remove(visitor:Visitor)
+synchronized getFloor(key:int):HashSet~Visitor~
+synchronized isFinished():boolean
+synchronized setFinished(finished:boolean)
+synchronized getVisitor()
}
class Structure {
<<interface>>
add(visitor:Visitor, key:int)
remove(visitor:Visitor, key:int)
getFloor(key:int):HashSet~Visitor~
getSize():int
exist(key:int):boolean
}
class NormalMap {
}
Structure <|-- NormalMap
Strategy <|-- Look
Input *-- VisitorList
Elevator *-- VisitorList
Elevator *-- Strategy
Elevator *-- Structure
VisitorList *-- Structure
Elevator --> Output
Structure o-- Visitor
时序图
sequenceDiagram
participant main as main<br><<Thread>>
participant in as input<br><<Thread>>
participant ele as elevator<br><<Thread>>
participant vl as visitorList
participant stra as strategy
activate in
main->>in:start
deactivate in
activate ele
main->>ele:start
deactivate ele
loop nextPersonRequset != null
activate vl
in->>vl:add
deactivate vl
end
loop gotoFloor != -1
activate stra
ele->>stra:gotoFloor
deactivate stra
activate vl
stra->>vl:getSize
stra->>vl:getEnd
stra->>vl:exist
deactivate vl
activate ele
stra->>ele:getSize
stra->>ele:exist
deactivate ele
end
activate vl
in->>vl:setEnd
deactivate vl
第二次作业
我的第二次作业相较于第一次作业没有类的增加,全部功能在第一次的基础上修改完成。因为环形电梯的功能和普通电梯的功能完全一样,唯一的区别在于二者移动的方向不一样,即前者移动时变的是building,而后者是floor。因此环形电梯只需要把普通电梯的floor和building交换即可。我为电梯新增一个属性cycle,用来代表是否是环形电梯,再为电梯增加maxFloor表示运行路径,便完成了环形电梯的功能。
然后是Look算法的改动,因为环形电梯可以转圈,因而不能和普通电梯一样调度。因此我的方法是根据Look基准选择目的地,然后看往哪边环绕近就往哪边走。
其次是这次作业中我使用了读写锁,但是是一次失败的尝试。
由于第二次作业改动实在太小,整体框架类图与时序图和第一次一模一样,因此这里不再重复展示,就展示修改了属性的电梯类吧。
classDiagram
class Elevator {
-list:VisitorList
-strategy:Strategy
-id:int
-nowBuilding:char
-nowFloor:int
-map:Structure
-size:int
-direction:int
-maxFloor:int
-cycle:boolean
-getActualBuilding()
-getActualFloor()
+run()
+moving()
+closeDoor()
+canIn(int floor):boolean
+canOut(int floor):boolean
+getIn(visitors:HashSet~Visitor~, flag:boolean)
+getOut(visitors:HashSet~Visitor~)
}
第三次作业
第三次作业相较于之前两次有较大的变化。由于环形电梯路径可自定义且乘客的路线有一定复杂性,因此这次作业我额外增加了较多类来实现这些功能。
首先需要说明的是,我为每种运行路径的电梯分配了一个候乘表,举例如下:
- 1号电梯路径:A-B-C
- 2号电梯路径:B-C-D
- 3号电梯路径:A-B-C
这种情况下,1号和3号使用同一候乘表,和第二次作业一样在这一候乘表中自由竞争,而2号独自完成所有任务。
为什么这么设置呢?因为我的策略是利用深度优先搜索为乘客规划一条耗时最短的路径,每段路径对应一个电梯,这在搜索完成时便已经确定,因此在乘客对应的阶段投入对应电梯等价于投入对应候乘表。这种思路更类似调度器,即直接将每个任务投入对应每个电梯。然而考虑到自由竞争的优异性,不直接确定分配给哪个电梯而是分配给哪个候乘表可能会具有更高效率。
另一个较大的改动就是调度器。由于乘客的任务被分解成多个阶段,因而抽离出一个对象专门完成乘客每个阶段的任务更合适。因此我完成了Controller类,在充分考虑线程安全问题的基础上进行乘客到候乘表的分配。
值得一提的是我在这次作业中使用了较多的单例模式,比如Input, Controller, Path(规划路径的类)。
这次作业较不合理的设计是我在Input类中加入太多的功能,没能抽离出一个新的类来实现输入请求的解析功能等,导致代码不满足单一功能。
类图
classDiagram
class Main {
+static main()
}
class Elevator {
-list:VisitorList
-strategy:Strategy
-id:int
-building:char
-map:Structure
-size:int
-fullSize:int
-direction:int
-cycle:boolean
-moveTime:int
+run()
+moving()
+closeDoor()
+getIn(visitors:HashSet~Visitor~, flag:boolean)
+getOut(visitors:HashSet~Visitor~)
}
class Input {
-static instanch:Input
-input:ElevatorInput
-lists:HashMap~Integer, VisitorList~
-cnt:int
-cntLock:Lock
-empty:Condition
+static getInstance():Input
-iniStart()
-addFloorEdge(arrive:int, floor:int, speed:int, eleId:int, listId:int)
-addBuildingEdge(startFloor:int, endFloor:int, building:char, eleId:int, listId:int)
+setEnd()
+finishVisitor()
+updatePaths()
+run()
}
class Output {
+static println(s:string)
+static printElevator()
+static printVisitor
}
class Strategy {
<<interface>>
gotoFloor(elevator:Structure,list:VisitorList, nowFloor:int, direction:int, size:int)
}
class Look {
-safe(floor:int)
+gotoFloor()
}
class Controller {
-static controller:Controller
-static lists:HashMap~Integer, VisitorList~
-static end:boolean
-static lock:Lock
-static empty:Condition
-static queue:Queue~Visitor~
+static initial(actualLists:HashMap~Integer,VisitorList~)
+static getInstance():Controller
+addVisitor(o:Visitor)
+sendVisitor(o:Visitor)
+senEnd()
+run()
}
class Visitor {
-id:int
-startFloor:int
-endFloor:int
-startBuilding:char
-endBuilding:char
-arriveTime:int
+setStatus(startFloor:int, endFloor:int, startBuilding:char, endBuilding:char)
}
class VisitorList {
-list:Structure
-finished:boolean
-lock:Lock
-empty:Condition
+add(visitor:Visitor)
+remove(visitor:Visitor)
+getFloor(key:int):HashSet~Visitor~
+isFinished():boolean
+setFinished(finished:boolean)
+getVisitor()
+lock()
+unlock()
}
class Structure {
<<interface>>
add(visitor:Visitor, key:int)
remove(visitor:Visitor, key:int)
getFloor(key:int):HashSet~Visitor~
getSize():int
exist(key:int):boolean
}
class NormalMap {
}
class Destination {
-to:int
-value:int
-id:int
}
class Path {
-static path:Path
-static edges:HashMap~Integer,Vector~
-static ele2list:HashMap~Integer,Integer~
-min: int
-vis:boolean[]
-pre:Deque~Integer~
-pos:Deque~Integer~
-ans:Deque~Integer~
-finalPos:Deque~Integer~
+static getInstance():Path
+addList(eleId:int, listId:int)
+addEdge(u:int, v:int, w:int, eleId:int)
+toPoint(floor:int, building:char):int
+toTask(listId:int, start:int, end:int):Task
+getPath(s:int, t:int):Queue~Task~
+dfs(nowId:int, nowElevator:int, time:int, target:int)
}
class Task {
-listId:int
-startBuilding:char
-startFloor:int
-endBuilding:char
-endFloor:int
}
Structure <|-- NormalMap
Strategy <|-- Look
Input *-- VisitorList
Elevator *-- VisitorList
Elevator *-- Strategy
Elevator *-- Structure
VisitorList *-- Structure
Elevator --> Output
Structure o-- Visitor
Path o-- Destination
Visitor *-- Task
Input --> Controller
Elevator --> Controller
Input --> Path
时序图
sequenceDiagram
participant main as main<br><<Thread>>
participant in as input<br><<Thread>>
participant path
participant con as controller<br><<Thread>>
participant ele as elevator<br><<Thread>>
participant vl as visitorList
participant v as visitor
participant stra as strategy
activate in
main->>in:start
deactivate in
activate ele
main->>ele:start
deactivate ele
loop nextRequset != null
activate vl
in->>vl:add
deactivate vl
activate con
in->>con:addVisitor
activate ele
in->>ele:new Elevator
deactivate ele
activate path
in->>path:getPath
deactivate path
end
loop gotoFloor != -1
activate stra
ele->>stra:gotoFloor
deactivate stra
activate vl
stra->>vl:getSize
stra->>vl:getEnd
stra->>vl:exist
deactivate vl
activate ele
stra->>ele:getSize
stra->>ele:exist
ele->>con:addVisitor
deactivate con
deactivate ele
end
loop !end
activate con
con->>con:sendVisitor
con->>v:setStatus
deactivate con
end
activate vl
in->>vl:setEnd
deactivate vl
4. 程序bug
三次作业功能实现上比较容易,主要出现bug的部分还是线程安全问题。
在第二、三次作业中我遇到了一些bug。在前文也提到过,我在第二次作业中使用了ReadWriteLock将读写分离,并试图通过实现线程安全的候乘表类来完成其功能。但在使用过程中不仅需要保证使用的每个方法是线程安全的,还需要多个方法的组合是原子的、线程安全的。因此,我的程序在此处出现了bug,在互测中被其他同学hack。
第三次作业我在可下实现的时候遇到一些bug。印象深刻的是一个死锁问题。最初电梯出人时这一动作是在获取该电梯对应候乘表的锁时进行的(图方便,和进人一起操作了)。这次为乘客分配任务阶段后,需要在完成当前任务后放入调度器继续进行下一阶段的任务。然而放入另一候乘表需要获取对应候乘表的锁,这便满足了死锁第一条件:先后获取两个锁。我之前还对这种死锁不屑一顾,认为锁的嵌套完全可以避免,然而现实中锁的获取隐藏在其他方法的实现中,当前方法只能看到调用了那个方法,却看不到这个方法里还有获取锁的操作。如此便导致死锁问题在不知不觉中产生了。这个bug的解决也很简单,在电梯出人后释放锁资源再将其投入调度器即可。现在回想这个bug,最主要的原因还是我当时没把调度器设置为一个线程,而是将分配资源融入当前线程中,设计不合理,响应性低、不满足封装原则而且效率低。后来我将调度器设置为了独立线程,想必这个bug也会迎刃而解了。
5. 发现bug的策略
在第一次作业中,我同往常一样写了自动测试脚本,随即生成数据并主要针对几种情况测试正确性。然而在第一次互测中,我的脚本跑了一天未能发现任何功能性bug,最后还是只hack了输出不安全。
后来我感觉随机生成数据寻找bug不太理想,便不再写测试脚本,而是手动构造典型样例运行。比如测试多个电梯之间线程安全,可以在同一楼层加入多个电梯后生成这一楼层的数个请求。通过构造典型数据,我在第二次互测中成功hack了几位同学。但第三次作业自己构造数据不太理想,因为判断对应数据运行结果的正确性也需要相应的程序辅助判断(肉眼很难看出来),而这又基本等同于从零开始完成自动测试。因此我在第三次作业的互测中没有太多贡献。
6. 心得体会
第二单元最大的体会还是多线程的设计。
- 高并发的设计需要让每个不相关的任务独立,彼此独立地执行自己的任务
- 线程之间需要交流,通过合理设置共享对象实现
- 共享对象要做好保护措施,不能产生线程安全问题
- 避免死锁行为,仔细检查是否有锁的嵌套
- 多学习多线程设计模式,掌握设计方法