摘要
这一单元的面向对象作业主要是对多线程编程的应用和理解Java程序运行时的原理。在完成作业时的一大难点就是对线程间共享资源的访问和通过wait()
和notifyAll()
方法来对多线程进行调度。这一部分对于我这种小白来说还是有些抽象,在实际操作过程中也是在慢慢摸索,尝试去理解多线程运行的方式。这里推荐《图解Java多线程设计模式》,这本书比较浅显易懂,对我理解多线程起到了很大作用。
一、关于三次作业的设计思路
由于在第一次作业就没有完全读懂指导书上的ALS算法,所以在三次作业的设计中,每部电梯的都采用了一种类似于LOOK的算法,说类似是本来是按照LOOK算法来写的,但是最终的性能分很低,但是听说其他用LOOK算法的同学的性能分都比较高,最终也不知道是哪里出了问题。在设计模式上,第二、三次作业也与第一次作业有所差异。
第一次作业:单部可稍带电梯
第一次作业相比较去年来说难度有所提升,需要考虑电梯的捎带问题和对路线规划的性能问题。对此采用了生产者—消费者模式,由主线程来启动生产者线程(Submission)和消费者线程(Elevator),它们共享一个请求队列( RuquestList )。电梯的运行状态由电梯内部来控制,相当于是在电梯内部放置了一个调度器来调度电梯。
1. 架构方面
1.1设计策略
MainClass中的main方法是主线程,用来创建一个共享的RequestListt实例并传入Submisson类和Eelvator类,然后启动这两个线程。
RequestList类实际上就是一个当作缓冲区的容器,用来存放Submission类新加入的请求,并且电梯也会从请求队列中有选择地(具体怎么选择涉及到电梯的调度算法,会在Elevator类中说明)取出请求。
Submission类就是仿照官方包中的Demo来实现,若读到有效请求,便将这一请求加入到请求队列中。有一点区别是在请求队列中有一个boolean类型的exitAble字段,一开始初始化为false,一旦Submission中读到了null(也就是读完了),就将RequestList中的exitAble设为true。
Elevator类里面其实就是实现了电梯的调度算法,可扩展性程度一般,后两次作业的电梯类均是以第一次作业为基础进行迭代开发。
在Elevator的run方法中,用一个while(true)的循环将所有操作包围起来,每一次循环视为到达一个新的楼层并进行相应操作,我将这些操作大概分为三个步骤:
- 对电梯进行进出操作:查看电梯内的请求是否到达,查看请求队列中是否有请求的出发楼层与电梯的当前楼层一致,并且与电梯运行方向相同,若有进出,则进行相应操作;
- 判断电梯下一步的运行方向:若电梯内部为空,则在请求队列中取出一个合适的请求设为buffer(一般为队首),让电梯去接buffer,若不为空,则按照根据电梯内部请求判断运行方向;
- 移动电梯(就按照上一步的运行方向移动一步就完了...)
1.2 SOLID原则
- SRP:由于只有两个线程,分工较明确,有一缺点是将控制电梯运行方向的调度器融合进了电梯类内部
(类似于电梯长了脑子,知道自己该往哪走),但这样带来的好处就是在第三次作业时可以直接往电梯类中添加功能,也不会在电梯过多时累死调度器。 - OCP:可扩展性较强
- LSP:无继承关系
- ISP: Elevator类和Submission类实现了Runnable接口
- DIP:
好像没什么关系
1.3 复杂度分析
Class metrics
class | OCavg | WMC |
---|---|---|
MainClass | 1.00 | 1 |
RequestList | 2.75 | 22 |
Elevator | 3.50 | 28 |
Submission | 2.00 | 4 |
Method metrics
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(RequestList) | 1 | 1 | 1 |
Elevator.IN(List ) | 1 | 2 | 2 |
Elevator.Out(List ) | 1 | 2 | 2 |
Elevator.judgeDirection(int,int) | 1 | 1 | 1 |
Elevator.run() | 6 | 10 | 12 |
Elevator.sameSide(PersonRequest) | 1 | 1 | 2 |
Elevator.selectIn() | 1 | 6 | 7 |
Elevator.selectOut() | 1 | 3 | 3 |
MainClass.main(String[]) | 1 | 1 | 1 |
RequestList.add(PersonRequest) | 1 | 1 | 1 |
RequestList.checkOtherSide(int,int) | 8 | 6 | 8 |
RequestList.get(int) | 1 | 1 | 1 |
RequestList.getBuffer(int) | 4 | 8 | 9 |
RequestList.getLength() | 1 | 1 | 1 |
RequestList.remove(PersonRequest) | 1 | 1 | 1 |
RequestList.remove(int) | 1 | 1 | 1 |
RequestList.setExitAble(boolean) | 1 | 1 | 1 |
Submission.Submission(RequestList) | 1 | 1 | 1 |
Submission.run() | 3 | 3 | 3 |
1.4 时序图
1.5 同步控制
这一次作业就是完成了一个简易的单生产者—单消费者模型,在同步控制方面只需要在RequestList中的方法设为synchronized,确保了同时只有一个消费者或一个生产者对RequestList进行操作。
当Elevator中没有请求并且buffer也为空时,若exitAble为false则开始wait()
。每当RequestList中加入新的请求时notifyAll()
。
2. Bug分析
此次作业在强测中所有测试点均通过,但是由于调度算法的问题性能分很低,最终只有85分
在互测中同样没有被hack到,在hack别人的过程中发现了很严重的死锁问题,只用了3条很简单的请求组合在一起便同时hack到两个人
第二次作业:多部可稍带电梯(增加了地下楼层)
刚刚拿到指导书时觉得增加的需求比较简单,刚打算还是沿用第一次作业的模式。直到周五白天开始上手做的时候才发现事情并不简单。
1. 架构方面
分析指导书后发现跟第一次作业相比主要增加了三方面的需求:
- 规定了最大载客量
- 电梯活动范围由3-15层扩展为-3层到-1层,1层到19层
- 多部电梯同时待命并进行工作
分析之后发现第1个和第2个新需求都比较好解决,第1个只要在电梯进人(请求)时增加一个判断条件,达到最大载客量时停止进人(请求)的动作即可,第2个请求只需在电梯改变楼层时若改变后电梯层数为0,则再在当前方向上再移动一个楼层即可。
但是第三个请求涉及到整体的架构设计,让我思考了(翻讨论区)很久,最终对第一次作业中的生产者—消费者模式进行一定的修改。
1.1 设计策略
MainClass的主要目的还是启动所有的电梯线程和调度器Scheduler线程,其中一个细节就是在main方法中创建了一个ElevatorInput实例,并且将它传入了Scheduler中,目的是在main中输入的电梯数量,剩下请求的输入都放在Scheduler类中。
Elevator类与第一次作业中的相比,区别在于,每部电梯中都保存这自己将要服务的请求队列List
这个服务队列是每个电梯独有的,不与其他电梯的服务队列重复,也不会与其他电梯产生交互。也就是将第一次作业中的RequestList当作了一部电梯内部的属性,也让电梯更加智能,直到自己将来要服务那些请求。
Scheduler类大体上与上一次作业中的Submission类相似,区别在于Scheduler类中有一个包含了所有Elevator对象的列表。Submission类在接收到一个新请求时会将它加入RequestList,而Scheduler在接收到新的时会立即访问所有的电梯对象,将新请求添加到一个合适的电梯的服务队列中去。
其中第二次作业的优化难点也在于对电梯请求的分配,我的做法是创建一个int[] time
数组,然后模拟每个电梯从当前位置移动到新请求的出发楼层所需的时间(其中包括了开关门等操作),将新请求加入到用时最短的电梯中去。
在程序结束的控制上,可以将exitAble信号设置为Elevator类的一个静态变量。
1.2 SOLID原则
由于将更多的功能都储存在了电梯内部,虽然让电梯更加智能,但是确实牺牲了一部分SRP原则做到的,这也是我的整个三次作业的一个缺点。
1.3 复杂度分析
Class metrics
Class | OCavg | WMC |
---|---|---|
Elevator | 2.72 | 49 |
MainClass | 3 | 3 |
Scheduler | 2.33 | 7 |
Method metrics
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Elevator.Elevator(int) | 2 | 2 | 7 |
Elevator.IN(List ) | 1 | 2 | 2 |
Elevator.Out(List ) | 1 | 2 | 2 |
Elevator.OverLoad() | 1 | 1 | 1 |
Elevator.addRequest(PersonRequest) | 1 | 1 | 1 |
Elevator.compareDirection(PersonRequest,PersonRequest) | 1 | 1 | 1 |
Elevator.getAimfloor() | 1 | 1 | 1 |
Elevator.getFloorNum() | 1 | 1 | 1 |
Elevator.getInside() | 1 | 1 | 1 |
Elevator.getMoveFlag() | 1 | 1 | 1 |
Elevator.getService() | 1 | 1 | 1 |
Elevator.judgeDirection(int,int) | 1 | 1 | 1 |
Elevator.run() | 4 | 11 | 13 |
Elevator.sameSide(PersonRequest) | 1 | 1 | 2 |
Elevator.selectIn() | 3 | 7 | 8 |
Elevator.selectOut() | 1 | 3 | 3 |
Elevator.setExitAble() | 1 | 1 | 1 |
Elevator.update() | 1 | 6 | 6 |
MainClass.main(String[]) | 1 | 3 | 3 |
Scheduler.Scheduler(ElevatorInput,List ) | 1 | 1 | 1 |
Scheduler.dispatch(PersonRequest,int) | 1 | 1 | 1 |
Scheduler.run() | 3 | 5 | 6 |
1.4 时序图
1.5 同步控制
相对于第一次作业的单生产者—单消费者的设计模式,第二次作业增加了更多的消费者,这就需要在各个消费者之间进行同步控制。
有了第一次作业对多线程熟悉的基础,在线程的调度和操控方面能够更加得心应手一些,其他的同步控制上与第一次作业类似。
2. Bug分析
正确性方面在强测和互测时都没有发生什么问题。但是性能上面还是比较差,在强测的20个测试点中有18个都是只有80分,性能方面依旧需要改进。
第三次作业:多部可捎带电梯(限制停靠楼层)
这次作业的需求主要增加了如下两个方面:
- 电梯被根据可停靠楼层分成了三种不同类型的电梯,每种电梯的最大载客量,运行速度,可停靠楼层都有所区别
- 电梯数量是动态的,可随时增加各种类型的电梯
需求分析:由于限制了可停靠楼层,本次作业的一大难点就是某些请求无法由一部电梯来直达,只能通过换乘的方式来对请求进行处理。
1. 架构方面
我的解决方法是将请求在尽量不绕路的原则下进行分割,将不可由一部电梯直达的请求分不同情况分割成两个请求,要点在于找到中间的换乘楼层。再将分割后的请求分派到各个电梯,这样就保证了被分派的所有请求都是可直达的。
但是这样带来一个问题,有可能人还没有到达换乘的楼层,下一部电梯就将后一半请求接走了,这显然是不合理的。对此我添加了Person类来保存分割前完整的请求,Floor类来储存当前楼层中的人。
1.1 设计策略
主类中的main()
方法用来初始化三个初始电梯,一个调度器和所有楼层,以及启动所有线程。
Scheduler类在第二次作业的基础上,增加了分割请求的功能。当接收到一个PersonRequest类型的请求时对请求进行合理的分割,并在请求的出发楼层中添加一个Person。当接收到一个ElevatorRequest类型的请求使,采用了工厂模式来创建一个相应类型的电梯线程并启动它。
Elevator类在第二次作业的基础上增加了“运行时间”,“最大载客量”,“可停靠楼层”,“电梯内人员id”的字段。这样处理可以使三种类型的电梯都通过对这一个类的实例化来完成。
AccessedFloor类的设置类似于第一单元作业中的Expression类,是为了使用方便,只存储了三类电梯可停靠楼层的一个类。
Channel类中储存了所有电梯的状态和信息用于唤醒其他电梯。
1.2 换乘策略
从图中可以清晰地看出,1层和15层时所有电梯都可以停靠的地点,但是为了不绕路,还得分以下6种情况进行讨论:
- A上,B下:若出发楼层是-3层,则换乘楼层为-2层;其他情况均在15层换乘
- A上,C下:若出发楼层>15层,则换乘楼层为15层;其他情况均在1层换乘
- B上,A下:若目的楼层是-3层,则换乘楼层在为-1层;其他情况均在15层换乘
- B上,C下:目的楼层必为3层。若出发楼层<=2,则换乘楼层在1层;其他其他情况均在5层换乘
- C上,A下:若请求方向为下行,则在1层换乘;若请求方向为上行在15层换乘
- C上,B下:出发楼层必为3层。若目的楼层<=4,则换乘楼层在1层;其他情况在(目的楼层-1)层换乘
1.3 SOLID原则
与第二次作业相同
1.4 复杂度分析
Class metrics
Class | OCavg | WMC |
---|---|---|
AccessedFloor | n/a | 0 |
Channel | 1.5 | 3 |
Elevator | 3.2 | 48 |
ElevatorFactory | 3 | 3 |
Floor | 1.5 | 6 |
MainClass | 4 | 4 |
Person | 1 | 3 |
SafeOutPut | 1 | 1 |
Scheduler | 5.14 | 36 |
Method metrics
Method | ev(G) | iv(G) | v(G) |
---|---|---|---|
Channel.notifyAllElevator() | 1 | 2 | 2 |
Channel.setElevators(List ) | 1 | 1 | 1 |
Elevator.Elevator(String,String,int,int,Set ,List ,Channel) | 1 | 1 | 1 |
Elevator.IN(List ,int) | 1 | 2 | 2 |
Elevator.Out(List ,int) | 4 | 4 | 5 |
Elevator.addRequest(PersonRequest) | 1 | 1 | 1 |
Elevator.compareDirection(PersonRequest,PersonRequest) | 1 | 1 | 1 |
Elevator.getFloorIndex() | 2 | 1 | 2 |
Elevator.getType() | 1 | 1 | 1 |
Elevator.judgeDirection(int,int) | 1 | 1 | 1 |
Elevator.notifyAllElevator() | 1 | 1 | 1 |
Elevator.run() | 4 | 12 | 14 |
Elevator.sameSide(PersonRequest) | 1 | 1 | 2 |
Elevator.selectIn(int) | 3 | 11 | 12 |
Elevator.selectOut() | 1 | 3 | 3 |
Elevator.setExitAble() | 1 | 1 | 1 |
Elevator.update() | 1 | 6 | 6 |
ElevatorFactory.produce(ElevatorRequest,List ,Channel) | 3 | 2 | 3 |
Floor.Floor(int) | 1 | 1 | 1 |
Floor.addPerson(Person) | 1 | 1 | 1 |
Floor.getPerson(int) | 1 | 2 | 3 |
Floor.remove(Person) | 1 | 1 | 1 |
MainClass.main(String[]) | 3 | 3 | 4 |
Person.Person(PersonRequest) | 1 | 1 | 1 |
Person.arrived(int) | 1 | 1 | 1 |
Person.getId() | 1 | 1 | 1 |
SafeOutPut.println(String) | 1 | 1 | 1 |
Scheduler.Scheduler(List ,List ,Channel) | 1 | 1 | 1 |
Scheduler.dispatch(List ) | 1 | 8 | 11 |
Scheduler.getFloorNum(int) | 2 | 1 | 2 |
Scheduler.putElevatorRequest(ElevatorRequest) | 1 | 1 | 1 |
Scheduler.requestSplit(PersonRequest,int,int) | 1 | 13 | 14 |
Scheduler.run() | 3 | 7 | 7 |
Scheduler.split(List ,PersonRequest,int) | 1 | 1 | 1 |
1.5 时序图
1.6 同步控制
与第二次作业基本相同
2. Bug分析
这次作业强测翻车了,并没有能够进入互测room...
在bug修复阶段发现强测20个点里面只过了1个,另外19个有的是WA有的是RTLE,经过分析发现了两个问题:
- 在B类电梯和C类电梯换乘时,换乘楼层本来应该是在 fromFloor-1 或 toFloor-1 结果全都写成了-1,然而C类电梯根本不可能在-1层停靠,造成了WA。
- 出现RTLE的问题便是死锁,原本设计的策略是只要这层有要加入电梯的请求(在不超载的情况下),就等待要加入的这个人到达该楼层。这样就有可能出现的问题是:B电梯在等A电梯送来人,而同时A电梯又在等待B电梯送来另一个人,这就将所有电梯都进入了睡眠状态,形成死锁。后来的解决方式是:若电梯中此时没人且服务队列中只有这一个请求时才进入等待,其他情况直接跳过。这样便成功AC了所有测试点。
二、心得体会
以前坐电梯的时候总是会思考电梯到底是怎样进行调度的,但是如今写完这一单元的作业,最大的体会就是再也无法直视电梯,当然经过了三周的实践,对多线程并发运行有了更深刻的体会和理解。在两次实验课上,也掌握了除生产者—消费者模式以外的几种设计模式,还在第三次作业中回顾了第一单元所学到的工厂模式。
这三次作业我都是通过自行构造请求队列来手动输入进行测试的,从最后一次强测翻车来看这样显然是不行的,在之后的作业中要尽量腾出实践来写一些自动化评测来找出bug。
最后还有要注意的就是性能问题,这一单元普遍性能分都非常低,应该是从第一次作业开始没有读懂LOOK算法导致的,今后还要在保证正确性的基础上尽可能地提高性能。