OO_Lab1
问题描述
模拟多线程实时电梯系统,新主楼ABCDE五个楼座各楼层均有电梯,乘客发起形如“从X座x层到Y座y层”的请求,电梯模拟上下行、开关门、乘客进出等行为,以满足所有乘客的要求。
解决思路
各个电梯无论是具体行为还是调度请求都相互独立,因此可以采用多线程的设计思想,每个电梯建立一个线程,这样电梯的行为都可以在线程内完成,可以非常方便的使用 sleep, wait, notifyAll
等函数来模拟消耗时间的操作和等待请求,同时输入也可以采用一个线程来处理。
整体的架构方面,三次作业我都采用了生产者-消费者模型,也就是刚才所提,输入作为生产者写为线程,该线程不断读取输入并提供给托盘;电梯作为消费者处理托盘的输入,每个电梯也写为一个线程;托盘模拟了容纳未处理的请求的容器,供输入线程与电梯线程交互使用。托盘的所有方法全部加锁以防止出现线程安全错误。
由于我在大部分情况下都采用了自由竞争策略,因此不需要显示的设置调度器,而是将请求的分解和分配分别放在输入和电梯线程中,而托盘只设置 addRequest, query, get
等操作。
第五次作业
这次作业较为简单,输入只涉及提供请求,电梯从楼座中选择请求接受,楼座(即托盘)将请求分类存储。
UML类图:
时序图:
后两次作业UML时序图与之相似,不再额外画出。
第六次作业
这次作业新增了横向电梯,但是其实没有什么本质改变,我采用了建立电梯抽象类的方法,仅将少部分横向、纵向电梯不同的方法设置为抽象方法,大部分电梯开关门、上下行、乘客进出对于两种电梯没有区别,可以在抽象电梯类中实现。同时将楼座和楼层抽象为容器(Container)和站点(Station)。对于加入新电梯的请求,只要新开一个对应线程即可。
UML类图:
第七次作业
首先自定义的要求非常容易处理,只要将那些部分从常量变成变量或方法即可。
难点在于请求的分解,我采用了静态分解的方式,也就是在请求被发起时就将其分解为若干横向和纵向请求,因为具体的数据分布无法得知,因此采用静态分配能较大幅度减少编码复杂度。
UML类图:
架构分析
基于度量的分析
对第七次作业进行基于度量的分析(只保留部分):
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
code.Elevator.run() | 25.0 | 7.0 | 10.0 | 16.0 |
code.CompoundRequest.solve() | 19.0 | 1.0 | 12.0 | 12.0 |
code.FloorElevator.getNextState(int) | 16.0 | 7.0 | 7.0 | 12.0 |
code.Elevator.checkEnter() | 13.0 | 4.0 | 6.0 | 8.0 |
code.BuildingElevator.getNextState(int) | 12.0 | 9.0 | 5.0 | 9.0 |
code.Elevator.checkExit() | 7.0 | 4.0 | 4.0 | 6.0 |
code.Container.connect(char, char) | 6.0 | 4.0 | 4.0 | 6.0 |
code.PassengerRequester.run() | 6.0 | 3.0 | 5.0 | 5.0 |
code.DirectRequest.DirectRequest(int, int, char, char, Container) | 5.0 | 2.0 | 2.0 | 5.0 |
code.PassengerRequester.addRequest(Request) | 5.0 | 1.0 | 3.0 | 3.0 |
code.Elevator.doorOpen() | 4.0 | 3.0 | 3.0 | 5.0 |
code.Elevator.moveDown() | 4.0 | 2.0 | 4.0 | 5.0 |
code.Elevator.moveUp() | 4.0 | 2.0 | 4.0 | 5.0 |
code.Elevator.passengerExit(CompoundRequest) | 4.0 | 2.0 | 4.0 | 5.0 |
code.Station.getDown(Elevator) | 4.0 | 3.0 | 4.0 | 4.0 |
code.Station.getUp(Elevator) | 4.0 | 3.0 | 4.0 | 4.0 |
code.Station.queryDown(Elevator) | 4.0 | 3.0 | 3.0 | 4.0 |
code.Station.queryUp(Elevator) | 4.0 | 3.0 | 3.0 | 4.0 |
code.Elevator.elevatorEmpty() | 3.0 | 3.0 | 2.0 | 3.0 |
code.Elevator.passengerEnter(CompoundRequest) | 3.0 | 2.0 | 3.0 | 4.0 |
code.Main.main(String[]) | 3.0 | 1.0 | 4.0 | 4.0 |
code.PassengerRequester.addCompoundRequest(CompoundRequest) | 3.0 | 2.0 | 2.0 | 3.0 |
code.CompoundRequest.getFirstRequest() | 2.0 | 2.0 | 2.0 | 2.0 |
code.CompoundRequest.tim(int) | 2.0 | 1.0 | 1.0 | 3.0 |
code.Container.waitForNext() | 2.0 | 1.0 | 3.0 | 3.0 |
code.DirectRequest.buildingName() | 2.0 | 2.0 | 2.0 | 3.0 |
code.DirectRequest.floorName() | 2.0 | 2.0 | 2.0 | 3.0 |
code.DirectRequest.getFromStation() | 2.0 | 2.0 | 2.0 | 2.0 |
code.DirectRequest.getToStation() | 2.0 | 2.0 | 2.0 | 2.0 |
code.DirectRequest.up() | 2.0 | 2.0 | 1.0 | 2.0 |
code.Elevator.doorClose() | 2.0 | 2.0 | 2.0 | 3.0 |
code.Station.addRequest(CompoundRequest, boolean) | 2.0 | 1.0 | 2.0 | 2.0 |
Total | 187.0 | 142.0 | 176.0 | 220.0 |
Average | 2.174418 | 1.651162 | 2.046511 | 2.558139 |
其中复杂度最高的几个方法 Elevator.run
,Elevator.checkEnter()
,Elevator.checkExit()
是电梯运行的核心部分, CompoundRequest.solve()
, FloorElevator.getNextState(int)
, BuildingElevator.getNextState(int)
是电梯调度的核心部分,这两部分决定了大部分电梯运行的主要逻辑,也是我编写代码主要思考的部分。
总的来讲,这些复杂度数值还在可以接受的范围内。
优点
- 思路清晰:各个电梯分为各个线程,各个电梯只需处理自己的行为,极大简化了代码编写。电梯、输入、托盘各司其职职责明确,分别提供有限的接口用于线程交互,在编码时不容易出错。各类耦合程度低而内聚程度高。
- 抽象程度高:将两类电梯都抽象为电梯实现上下行、开关门、乘客进出;将楼层楼座都抽象为容器和站点的组合,只需使用同一套代码即可解决这两类问题,抽象程度到减少了代码量,也降低了编码和调试难度。
- 线程更安全:输入线程与电梯线程、电梯线程之间不直接交互,而是全部通过容器来进行交互,因此只要两类线程都只调用容器的方法,同时对容器加锁,那么就理论上不存在线程安全问题。这可以简化线程的交互,也不用考虑特殊情况是否会出问题。电梯的停止也是由容器来判断。
细节实现
1、复杂请求的分解
如果是纵向请求,则直接加入对应容器,否则选择总距离最短、换乘次数最少、接受任务最少的横向电梯作为中转,代码如下:
public void solve() {
steps.clear();
if (this.fromBuilding == this.toBuilding) {
Container container = containers.get(fromBuilding);
steps.add(new DirectRequest(fromFloor, toFloor, fromBuilding, toBuilding, container));
container.addUnfinishedRequest();
} else {
int dis = Integer.MAX_VALUE;
int tim = Integer.MAX_VALUE;
int unf = Integer.MAX_VALUE;
int floor = 1;
for (int i = 1; i <= Main.getFloorCount(); ++i) {
Container container = containers.get(Main.floorIdToName(i));
if (container.connect(fromBuilding, toBuilding)) {
if (dis(i) < dis) {
floor = i;
dis = dis(i);
tim = tim(i);
unf = container.getUnfinishedRequest();
} else if (dis(i) == dis && tim(i) < tim) {
floor = i;
tim = tim(i);
unf = container.getUnfinishedRequest();
} else if (dis(i) == dis && tim(i) == tim && container.getUnfinishedRequest() < unf) {
floor = i;
unf = container.getUnfinishedRequest();
}
}
}
if (this.fromFloor != floor) {
Container container1 = containers.get(fromBuilding);
steps.add(new DirectRequest(fromFloor, floor, fromBuilding, fromBuilding, container1));
container1.addUnfinishedRequest();
}
Container container2 = containers.get(Main.floorIdToName(floor));
steps.add(new DirectRequest(floor, floor, fromBuilding, toBuilding, container2));
container2.addUnfinishedRequest();
if (floor != this.toFloor) {
Container container3 = containers.get(toBuilding);
steps.add(new DirectRequest(floor, toFloor, toBuilding, toBuilding, container3));
container3.addUnfinishedRequest();
}
}
}
2、线程的结束
线程的结束可以理解为特殊的请求,即输入线程请求结束整个程序,然后由容器(托盘)向电梯发出停止的请求,电梯在执行完所有请求后结束自身。
PassengerRequester:
for (Character key : containers.keySet()) {
containers.get(key).requestTerminate();
}
Container:
private void checkStop() {
if (stop()) {
notifyAll();
}
}
public synchronized boolean stop() {
return requestTerminate && unfinishedRequest == 0;
}
public synchronized void requestTerminate() {
requestTerminate = true;
checkStop();
}
Elevator:
private boolean elevatorEmpty() {
for (Queue<CompoundRequest> q : this.acceptedRequests) {
if (!q.isEmpty()) {
return false;
}
}
return true;
}
private boolean checkStop() {
return container.stop() && elevatorEmpty();
}
3、线程通信
采用 wait-notify 机制,在 Container 里进行通信:
public synchronized void addRequest(CompoundRequest compoundRequest) {
stations.get(compoundRequest.firstRequestFromStation())
.addRequest(compoundRequest,compoundRequest.firstRequestDirection());
notifyAll();
}
public synchronized void waitForNext() {
try {
if (!stop()) {
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
性能策略
1、电梯的请求查询
对于每个楼层、楼座,将所有请求按每层上下分到各个队列里。电梯使用 getNextState
来接收新请求。对于纵向电梯,优先选取在其上方向上的请求,对于横向电梯,优先选取离其最近的请求。
2、横向请求方向
我采用了将所有请求视为同一方向的策略。
3、多个电梯任务分配
我采用了自由竞争的方式,即各个电梯同时去找各个请求,先到先得,这样在简化代码难度的情况下也能保证不错的性能。
代码测试
我采用了随机生成数据与手动测试相结合的方式,通过随机大量数据来覆盖大部分错误,同时手动针对电梯超载、在错误楼层停下等bug进行测试。
发现的bug
1、多线程下,不能错误的认为已经判断过的情况接下来不会再发生。
我使用了 checkEnter
函数判断了是否有乘客进入,接下来错误的认为只有电梯移动和空等的可能,但是实际上可能在 checkEnter
后出现请求,使得我的电梯错误运行。