BUAA OO 第二单元总结
BUAA OO 第二单元总结
第一次作业
作业简介
简单的多线程实时电梯系统,五个楼座,10层楼,只有竖向电梯。
StarUML图与类结构
其中,各个类的含义如下:
|- MainClass:主类
|- Elevator:电梯类
|- Queue:请求队列类
|- Stdin:输入处理类
|- Output:输出类
SequenceDiagram
架构分析
程序分析
本次作业,是我第一次接触多线程编程任务,整体架构参考了实验课的代码。
大体思路是,输入处理类接收输入包的指令输入,对指令进行分析,将指令分配给对应电梯的请求队列Queue(在这里分配的原则是,根据请求所在楼座,分配给对应楼座的电梯)。请求进入电梯的请求队列后,按照电梯运行策略进行运动。
电梯运行策略,我采取了指导书上的ASL策略,实现了捎带,捎带策略与指导书基本相同。
电梯运行,我设置了State表示电梯当前的状态:
enum State { Rest_state, End_state, Open_state, Close_state,
In_state, Out_state, Up_state, Down_state }
并使用switch语句进行状态转换,框架如下:
public void run() {
while (true) {
switch (state) {
case Rest_state: {
......
break;
} case Up_state: {
......
break;
} case Down_state: {
......
break;
} case Open_state: {
......
break;
} case Close_state: {
......
break;
} case In_state: {
......
break;
} case Out_state: {
......
break;
} case End_state: {
return;
} default: {
break;
}
}
}
parser方法主要是用于获取在当前层可以进入电梯的队列(uplist & downlist)与可以出电梯的队列(outlist),框架如下:
public int[] parser(ArrayList<PersonRequest> uplist, ArrayList<PersonRequest> downlist) {
synchronized (queue) {
int upnum = 0;
int downnum = 0;
int outnum = 0;
for (遍历电梯请求等待队列) {
PersonRequest request = 取一个请求;
if (FromFloor为当前楼层) {
if (请求为向上请求) {
该请求加入uplist,upnum+1
} else if (请求为向下请求) {
该请求加入downlist,downnum+1
}
}
}
for (遍历已进入电梯的请求队列) {
if (ToFloor为当前楼层) {
该请求加入queueout,outnum+1
}
}
return new int[]{upnum, downnum, outnum};
}
}
捎带策略主要框架如下:
synchronized (queue) {
if (电梯方向向上) {
if (存在FromFloor为当前层的向上指令 || 当前层有人要出电梯) {
queuein设为uplist,电梯状态转换:->Open_state
}
} else if (电梯方向向下) {
if (存在FromFloor为当前层的向下指令 || 当前层有人要出电梯) {
queuein设为downlist,电梯状态转换:->Open_state
}
}
}
由于输出包线程不安全,我对于输出包进行了再封装,使用单例模式,建立Output类。
public class Output {
private static final Output OUTPUT_THREAD = new Output();
private Output(){}
public static Output getInstance() {
return OUTPUT_THREAD;
}
public synchronized void println(String msg) {
TimableOutput.println(msg);
}
}
由于此次电梯只有五个,且数目固定,我并没有建立电梯的list,而是直接在MainClass主类里面新建了5个电梯以及电梯对应的Queue,这显得主类有些臃肿,下次作业可以采用list的方式改掉。
同步块的设置和锁的选择
本次作业,共享资源只有每个座电梯的请求等待队列(被input和对应电梯,两个线程访问)和Output。
对于同步块的设置和锁的选择,我采用了简单的synchronized关键字。
Output类,我设置打印方法println为同步方法:
public synchronized void println(String msg) {
TimableOutput.println(msg);
}
对于等待队列,我在需要使用它们的地方,设置同步块:
synchronized (queue) {
queue操作(加指令、删指令或者其他)
}
性能优化
1、实现捎带策略。
程序分析
度量分析
class | OCavg | OCmax | WMC |
---|---|---|---|
Stdin | 5.0 | 9.0 | 10.0 |
Elevator | 4.7272727272727275 | 21.0 | 52.0 |
MainClass | 1.0 | 1.0 | 1.0 |
Output | 1.0 | 1.0 | 3.0 |
Queue | 1.0 | 1.0 | 4.0 |
Elevator.State | 0.0 | ||
Total | 70.0 | ||
Average | 3.3333333333333335 | 6.6 | 11.666666666666666 |
OCavg
=Average opearation complexity
(平均操作复杂度)
OCmax
=Maximum operation complexity
(最大操作复杂度)
WMC
=Weighted method complexity
(加权方法复杂度)
可以看到,Stdin类和Elevator类复杂度较高,可能的原因在于这两个类都是线程类,且执行了整个电梯系统大部分的功能,功能较为复杂,时间开销大。
其余类的复杂度尚可。
方法复杂度分析
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Elevator.dispatch() | 56.0 | 1.0 | 12.0 | 25.0 |
Elevator.parser(ArrayList, ArrayList) | 10.0 | 1.0 | 7.0 | 7.0 |
Stdin.run() | 8.0 | 3.0 | 9.0 | 9.0 |
Elevator.stay() | 7.0 | 1.0 | 8.0 | 8.0 |
Elevator.in() | 3.0 | 3.0 | 2.0 | 3.0 |
Elevator.run() | 3.0 | 3.0 | 9.0 | 10.0 |
Elevator.close() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.down() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.open() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.out() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.up() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.Elevator(Queue, int, int, int, char, int, long, ...) | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.Output() | 0.0 | 1.0 | 1.0 | 1.0 |
Output.getInstance() | 0.0 | 1.0 | 1.0 | 1.0 |
Output.println(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.Queue(boolean, ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.getQueueisEnd() | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.getWaitQueue() | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.setQueueisEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Stdin.Stdin(Queue, Queue, Queue, Queue, Queue) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 92.0 | 27.0 | 67.0 | 82.0 |
Average | 4.380952380952381 | 1.2857142857142858 | 3.1904761904761907 | 3.9047619047619047 |
CogC
=Cognitive complexity
(认知复杂度)
ev(G)
=Essential cyclomatic complexity
(基本圈复杂度)
iv(G)
=Design complexity
(设计复杂度)
v(G)
=cyclonmatic complexity
(圈复杂度)
可以看到,dispatch调度方法复杂度较高,其余方法的圈复杂度尚可。
调度方法主要解决的问题是,请求从电梯请求等待队列进入电梯;捎带;电梯基于主请求的运行方向等问题,确实较为复杂,可能具体算法还有待改进,也可以考虑建立多个方法分摊一下。
Bug分析
程序设计过程中的bug
程序设计过程中遇到过很多bug,多数为细节处理的考虑不周造成的。
比如在第一次提交时,我遇到了RE的错误,经检查,发现CPU时间普遍很高。经过咨询助教,得知CPU时间一般在0-2s为宜,故锁定问题为存在轮询。经过分析代码架构,我发现我的程序每接收一个请求,会唤醒所有电梯,这是没必要的,A座的请求只需要唤醒A座的电梯即可。如果请求会唤醒所有电梯,则在大部分时间,电梯都会空转浪费CPU资源。改正此点后解决了RE的错误。
公测中的bug
HW1的公测中,我的程序没有被检测出bug。
互测中的bug
HW1的互测中,我的程序被检测出了多组bug,均为同质bug。其原因是,没有考虑输出包的线程不安全性,导致时间戳并非非减输出。改正方案为封装为Output类。
public class Output {
private static final Output OUTPUT_THREAD = new Output();
private Output(){}
public static Output getInstance() {
return OUTPUT_THREAD;
}
public synchronized void println(String msg) {
TimableOutput.println(msg);
}
}
HW1的互测中,我发现了房间中其他同学的bug,均为上述的输出包线程不安全的问题。
测试思路
测试思路是 坑点针对测试+随机测试
其中坑点针对测试需要的是仔细分析指导书,找出指导书中的一些边界情况
针对这些边界情况或者易忽视的情况,构造测试数据进行测试
随机测试就是随机构造数据,可以自己编写(或白嫖)测试程序
我的感受
本次作业,我学会了多线程的基础知识、对象并发及其协同方法、线程安全保护、设计模式等内容。
HW1要求较为简单,只涉及竖向的单电梯楼座,对于大体的架构,由于有实验课程的帮助,问题不大。
但多线程设计,需要考虑的线程安全性等问题,则是一道难关。如果考虑不周会经常出现“意想不到”,“难以复现”的bug。如轮询等错误,在本地测试上甚至难以体现,在交到评测机上才发现CPU时间爆炸。
本次作业,我被hack到了,其原因是没好好看指导书,指导书上是有提示输出包线程不安全的问题的,但当时由于对于线程安全重要性认识不够,没有太过注意,导致bug出现,需要记下这个教训。
第二次作业
作业简介
较为复杂的多线程实时电梯系统,新增楼座与楼座之间移动的环形电梯,可以有多部电梯,电梯可动态增加。
StarUML图与类结构
其中,各个类的含义如下:
|- MainClass:主类
|- Schedule1:竖向楼座调度类
|- Schedule2:横向楼层调度类
|- Elevator1:竖向电梯类
|- Elevator2:横向电梯类
|- Queue:请求队列类
|- Input:输入处理类
|- Output:输出类
SequenceDiagram
架构分析
整体思路
本次作业,新增的需求有:
1、横向环形电梯
横向电梯其实本质上和竖向电梯区别不大,我类比竖向电梯进行横向电梯的设计。
我将横向电梯A->B方向类比为竖向电梯的向上,横向电梯A->E方向类比为竖向电梯的向下。竖向电梯向上楼层递增,向下楼层递减,而在横向电梯中也是如此,比如A->B,座号也是递增的,区别在于其是环形的,当E+1=F以后,需要特判一下,把F改为A(回到A座),A-1的情况同理改为E。
在竖向电梯中,请求自带方向属性(ToFloor与FromFloor大小关系),而在横向电梯中,请求也可以具有方向。参考指导书的方案,以最短路径方向作为指令的方向,比如A->B是“向上”,A->E是”向下“。
2、新增电梯
楼座(楼层)调度类里面有位于该楼座(楼层)的电梯list,以及每个电梯的请求等待list
private final ArrayList<Elevator1> elevators = new ArrayList<>();
private final ArrayList<Queue> queues = new ArrayList<>();
新增电梯,只需要分析请求,得出电梯应该加到哪个楼座或楼层,然后加入其调度类里面的list即可。
public void elevatorreq1(ElevatorRequest request) {
switch (request.getBuilding()) {
case 'A': {
将电梯放入A座
break;
}
case 'B': {
......
}
......
}
}
3、多部电梯
多部电梯带来的核心问题就是,对于一个请求,应该使用该楼座(楼层)的哪部电梯来接呢?
我采用的策略是请求分配策略,即请求进入时就为其分配一个电梯。
分配原则与大体框架如下:
for (遍历该楼座(楼层)的总请求队列) {
if (如果有电梯为空,正在wait) {
分配给空电梯。
如果有多部空电梯,则分配给离得最近的空电梯
}
else if (有正在运行的电梯可稍带) {
分配给可稍带电梯
如果有多部可稍带,则分配给当前状态下请求数最少的电梯
}
else {
分配给当前状态下请求数最少的电梯
}
}
同步块的设置和锁的选择
本次作业,共享资源有每个座电梯Elevator以及其对应的请求等待队列Queue、电梯调度器Schedule和Output。
对于同步块的设置和锁的选择,我采用了简单的synchronized关键字。
Output类,我设置打印方法println为同步方法:
public synchronized void println(String msg) {
TimableOutput.println(msg);
}
对于等待队列,我在需要使用它们的地方,设置同步块:
synchronized (queue) {
queue操作(加指令、删指令或者其他)
}
对于电梯调度器,我将其内部的方法,设为同步方法:
public synchronized void addRequest(PersonRequest p) {
do something
}
对于电梯类,我将其被多线程调用的方法,设为同步方法:
public synchronized String getTheState() {
do something
}
性能优化
1、实现捎带策略
2、多部电梯择优分配请求的策略
3、环形电梯按较短路径走,而不是一直一个方向
程序分析
度量分析
class | OCavg | OCmax | WMC |
---|---|---|---|
Input | 7.285714285714286 | 12.0 | 51.0 |
Elevator1 | 4.0 | 21.0 | 60.0 |
Elevator2 | 4.0 | 19.0 | 72.0 |
Schedule1 | 2.5454545454545454 | 6.0 | 28.0 |
Schedule2 | 2.3 | 6.0 | 23.0 |
MainClass | 1.0 | 1.0 | 1.0 |
Output | 1.0 | 1.0 | 3.0 |
Queue | 1.0 | 1.0 | 4.0 |
Elevator1.State | 0.0 | ||
Elevator2.State | 0.0 | ||
Total | 242.0 | ||
Average | 3.5072463768115942 | 8.375 | 24.2 |
OCavg
=Average opearation complexity
(平均操作复杂度)
OCmax
=Maximum operation complexity
(最大操作复杂度)
WMC
=Weighted method complexity
(加权方法复杂度)
可以看到Input类、Elevator1类、Elevator2类复杂度较高。
输入处理类平均操作复杂度较高,可能与多重条件判断与switch有关,两个电梯类复杂度较高可能与循环层数较多,条件判断较多等原因有关。
其余类复杂度尚可。
方法复杂度分析
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Elevator1.dispatch() | 56.0 | 1.0 | 12.0 | 25.0 |
Elevator2.dispatch() | 56.0 | 1.0 | 10.0 | 23.0 |
Input.run() | 19.0 | 3.0 | 8.0 | 8.0 |
Schedule1.run() | 14.0 | 3.0 | 9.0 | 9.0 |
Schedule2.run() | 14.0 | 3.0 | 9.0 | 9.0 |
Elevator1.parser(ArrayList, ArrayList) | 10.0 | 1.0 | 7.0 | 7.0 |
Elevator2.parser(ArrayList, ArrayList) | 10.0 | 1.0 | 7.0 | 7.0 |
Schedule1.takenable(PersonRequest) | 9.0 | 3.0 | 5.0 | 7.0 |
Elevator2.isup(PersonRequest) | 8.0 | 4.0 | 2.0 | 4.0 |
Elevator2.isupp(PersonRequest) | 8.0 | 4.0 | 2.0 | 4.0 |
Elevator2.isuppp(PersonRequest) | 8.0 | 4.0 | 2.0 | 4.0 |
Elevator1.stay() | 7.0 | 1.0 | 8.0 | 8.0 |
Elevator2.stay() | 7.0 | 1.0 | 8.0 | 8.0 |
Schedule1.available(PersonRequest) | 7.0 | 1.0 | 4.0 | 5.0 |
Schedule2.available(PersonRequest) | 7.0 | 1.0 | 4.0 | 5.0 |
Schedule1.setElevator(ArrayList) | 5.0 | 4.0 | 4.0 | 4.0 |
Elevator1.in() | 3.0 | 3.0 | 2.0 | 3.0 |
Elevator1.run() | 3.0 | 3.0 | 9.0 | 10.0 |
Elevator2.in() | 3.0 | 3.0 | 2.0 | 3.0 |
Elevator2.run() | 3.0 | 3.0 | 9.0 | 10.0 |
Schedule1.latest(PersonRequest) | 3.0 | 1.0 | 3.0 | 3.0 |
Schedule2.latest(PersonRequest) | 3.0 | 1.0 | 3.0 | 3.0 |
Schedule2.setElevator(Vector) | 3.0 | 3.0 | 3.0 | 3.0 |
Elevator1.getDirection() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator1.getTheState() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator2.down() | 2.0 | 1.0 | 2.0 | 3.0 |
Elevator2.getDirection() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator2.getTheState() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator2.up() | 2.0 | 1.0 | 2.0 | 3.0 |
Input.init() | 2.0 | 1.0 | 3.0 | 3.0 |
Input.setend() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator1.close() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator1.down() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator1.open() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator1.out() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator1.up() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator2.close() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator2.open() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator2.out() | 1.0 | 1.0 | 2.0 | 2.0 |
Input.elevatorreq1(ElevatorRequest) | 1.0 | 1.0 | 6.0 | 6.0 |
Input.elevatorreq2(ElevatorRequest) | 1.0 | 1.0 | 11.0 | 11.0 |
Input.personreq1(PersonRequest) | 1.0 | 1.0 | 6.0 | 6.0 |
Input.personreq2(PersonRequest) | 1.0 | 1.0 | 11.0 | 11.0 |
Schedule1.availElevatorIndex(PersonRequest) | 1.0 | 2.0 | 1.0 | 2.0 |
Schedule2.availElevatorIndex(PersonRequest) | 1.0 | 2.0 | 1.0 | 2.0 |
Elevator1.Elevator1(Queue, int, int, int, char, int, long, ...) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator1.getNowFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator1.getPeople() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.Elevator2(Queue, int, int, int, char, int, long, ...) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.getCurBuilding() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.getPeople() | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.Output() | 0.0 | 1.0 | 1.0 | 1.0 |
Output.getInstance() | 0.0 | 1.0 | 1.0 | 1.0 |
Output.println(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.Queue(boolean, ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.getQueueisEnd() | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.getWaitQueue() | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.setQueueisEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.Schedule1(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.addElevator(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.addRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.dispacher() | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.setEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.Schedule2(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.addElevator(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.addRequest(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.dispacher() | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.setEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 296.0 | 109.0 | 222.0 | 274.0 |
Average | 4.2898550724637685 | 1.5797101449275361 | 3.217391304347826 | 3.971014492753623 |
CogC
=
Cognitive complexity`(认知复杂度)
ev(G)
=Essential cyclomatic complexity
(基本圈复杂度)
iv(G)
=Design complexity
(设计复杂度)
v(G)
=cyclonmatic complexity
(圈复杂度)
可以看到,dispatch方法复杂度较高。调度方法主要解决的问题是,请求从电梯请求等待队列进入电梯;捎带;电梯基于主请求的运行方向等问题,确实较为复杂,可能具体算法还有待改进。
其余方法复杂度尚可。
Bug分析
程序设计过程中的bug
程序设计过程中遇到过的bug,多数为细节处理的考虑不周造成的。
比如横向电梯的运行,曾出现横向电梯反复横跳的问题。
公测中的bug
HW2的公测中,我的程序没有被检测出bug。
互测中的bug
HW2的互测中,我的程序没有被检测出bug。
HW2的互测中,我未能发现房间中其他同学的bug。
测试思路
测试思路是 坑点针对测试+随机测试
其中坑点针对测试需要的是仔细分析指导书,找出指导书中的一些边界情况
针对这些边界情况或者易忽视的情况,构造测试数据进行测试
随机测试就是随机构造数据,可以自己编写(或白嫖)测试程序
我的感受
本次作业,我对于多线程的协调、线程安全及其设计等有了更深入的认识。
在实验课上,也学到了服务器架构的线程交互与流水架构的线程交互,虽然流水线架构并没有在此次作业中使用(因为没人走”斜线“),但也许下次作业就会出现需要换乘的情况了,我觉得可以类比worker的工序设计来完成。
这次作业,相较于第一次作业,新增了横向环形电梯、可以有多部电梯、可以新增电梯,看似变化很大。但仔细思考会发现其实横向电梯和竖向电梯在整体架构上,实际上区别不大,完全可以类比。因此此次作业并没有花费太长时间,我继续沿用了第一次作业的架构,扩充了多部电梯的请求分配策略,扩充了支持新增电梯的方法。
第三次作业
作业简介
复杂的多线程实时电梯系统,横向电梯楼座停靠可配置,电梯容量和运行时间可配置,路径可达性问题(换乘)
StarUML图与类结构
其中,各个类的含义如下:
|- MainClass:主类
|- Controller:主控制类
|- Counter:请求计数类
|- Schedule1:竖向楼座调度类
|- Schedule2:横向楼层调度类
|- Elevator1:竖向电梯类
|- Elevator2:横向电梯类
|- Queue:请求队列类
|- Req:请求类
|- Input:输入处理类
|- Output:输出类
SequenceDiagram
架构分析
整体思路
本次作业,功能上与HW2相比,有所增强。
1、横向电梯楼座停靠可配置,电梯容量和运行时间可配置
由于原先设计,电梯容量、运行时间等就可以配置,此处无需更改。
对于可停靠问题,横向电梯类新增成员mm,以掩码的形式保存可停靠信息:
private int mm;
public synchronized int getmm() {
return mm;
}
新增getreachable方法判断是否可停靠:
public synchronized boolean getreachable(char building) {
if ( buiding可停靠 ) {
return true;
}
return false;
}
2、换乘处理
我的思路是,电梯个体的运营策略不变,通过对于请求的再封装来实现换乘,参考了实验课的流水架构。
请求再封装:建立Req类,Req类结构如下:
public class Req {
private PersonRequest request;
private int fromfloor;
private int tofloor;
private char frombuilding;
private char tobuilding;
public Req(PersonRequest request) {......}
public void setfromfloor(int a) {......}
public void settofloor(int a) {......}
public void setfrombuilding(char c) {......}
public void settobuilding(char c) {......}
public int getPersonId() { return request.getPersonId(); }
public int getFromFloor() { return fromfloor; }
public int getToFloor() { return tofloor; }
public char getFromBuilding() { return frombuilding; }
public char getToBuilding() { return tobuilding; }
public char gettrueToBuilding() {
return request.getToBuilding();
}
public int gettrueToFloor() {
return request.getToFloor();
}
}
可以看到的是Req类实际上与请求类成员属性上没有区别,它的特点是,支持更改起始地点与目的地点。
我的想法是,比如A-2 -> B-5的指令,现在我要采取A-2 -> A-1 -> B-1 -> B-5的方式,那么就先更改Req类的目标地点为A-1,然后当作正常的竖向请求放入A座电梯中。等到达了A-1以后,出电梯时判断是否到达最终目的地。
request.gettrueToBuilding()与request.gettrueToFloor() 返回最终目的地点。
如果到达了,就释放请求;如果没到达,以当前位置为起始地点,以最终目的为目标地点,设置Req,将其返回给主控制类进行再次分配。
public void out() {
for (Req request:queueout) {
......
if (request.gettrueToBuilding() != id || request.gettrueToFloor() != nowfloor) {
request.setfrombuilding(id);
request.setfromfloor(nowfloor);
request.settobuilding(request.gettrueToBuilding());
request.settofloor(request.gettrueToFloor());
Controller.getInstance().alloc(request);
} else {
Counter.getInstance().release();
}
......
}
}
Counter类:
这个类用来判断请求是否全部结束,很重要,如果没有这个请求判断会造成有指令还在换乘,但输入NULL,所有电梯停止(指令还没换乘结束)的bug。这个类的架构我完全采用了实验课的架构,不再赘述。
public class Counter {
private int count;
private static final Counter COUNTER = new Counter();
Counter() {
count = 0;
}
public static Counter getInstance() {
return COUNTER;
}
public synchronized void release() { //代表完成一个任务
......
}
public synchronized void acquire() { //检验一个任务的完成,如果没有已完成的任务,等待
......
}
}
换乘策略:
if (起始楼层存在横向电梯:可同时停靠起始楼座与最终楼座) {
//横向运动,故设置目的楼层为起始楼层,目的楼座为最终楼座
request.settofloor(request.getFromFloor());
request.settobuilding(request.getToBuilding());
将请求传入符合要求的横向电梯
return;
}
if (目标楼层存在横向电梯:可同时停靠起始楼座与最终楼座) {
//需要先到达目标楼层,故竖向运动,设置目的楼层为最终楼层,目的楼座为起始楼座
request.settofloor(request.getToFloor());
request.settobuilding(request.getFromBuilding());
将请求传入符合要求的竖向电梯
return;
}
if (某楼层存在横向电梯:可同时停靠起始楼座与最终楼座) {
如果存在多个楼层符合情况,则选取其中离起始楼层与最终楼层距离和最小的楼层
//需要先到达这一楼层,故竖向运动,设置目的楼层为这一楼层,目的楼座为起始楼座
request.settofloor(符合条件横向电梯所在楼层);
request.settobuilding(request.getFromBuilding());
将请求传入符合要求的竖向电梯
return;
}
同步块的设置和锁的选择
本次作业,共享资源有每个座的电梯,以及其对应的请求等待队列(被input和对应电梯,两个线程访问),电梯调度器类,Output,Controller,Counter。
对于同步块的设置和锁的选择,我采用了简单的synchronized关键字。
Output类,我设置打印方法println为同步方法:
public synchronized void println(String msg) {
TimableOutput.println(msg);
}
对于等待队列,我在需要使用它们的地方,设置同步块:
synchronized (queue) {
queue操作(加指令、删指令或者其他)
}
对于电梯调度器,我将其内部的方法,设为同步方法:
public synchronized void addRequest(PersonRequest p) {
do something
}
对于电梯类,我将其被多线程调用的方法,设为同步方法:
public synchronized String getTheState() {
do something
}
对于主控类,我设置同步块:
synchronized (this) {
do something
}
对于Counter类,我设置同步块:
public synchronized void release() { //代表完成一个任务
do something
}
性能优化
1、实现捎带策略。
2、支持选择最近距离的横向电梯换乘
3、支持横向电梯的二次换乘
4、多部电梯择优分配请求的策略
5、环形电梯按较短路径走,而不是一直一个方向
程序分析
度量分析
class | OCavg | OCmax | WMC |
---|---|---|---|
Input | 7.0 | 7.0 | 7.0 |
Controller | 5.375 | 12.0 | 43.0 |
Elevator1 | 3.7058823529411766 | 21.0 | 63.0 |
Elevator2 | 3.6666666666666665 | 19.0 | 77.0 |
Schedule2 | 3.0 | 9.0 | 24.0 |
Schedule1 | 2.5454545454545454 | 6.0 | 28.0 |
Counter | 1.5 | 3.0 | 6.0 |
MainClass | 1.0 | 1.0 | 1.0 |
Output | 1.0 | 1.0 | 3.0 |
Queue | 1.0 | 1.0 | 4.0 |
Req | 1.0 | 1.0 | 12.0 |
Elevator1.State | 0.0 | ||
Elevator2.State | 0.0 | ||
Total | 268.0 | ||
Average | 2.977777777777778 | 7.363636363636363 | 20.615384615384617 |
OCavg = Average opearation complexity(平均操作复杂度)
OCmax = Maximum operation complexity(最大操作复杂度)
WMC = Weighted method complexity(加权方法复杂度)
可以看到Input类、Controller类、Elevator类复杂度较高。
可能原因是主控Controller类的电梯换乘算法复杂度高,Elevator电梯运行策略复杂度较高。
其余类复杂度尚可。
方法复杂度分析
method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Elevator1.dispatch() | 58.0 | 1.0 | 12.0 | 27.0 |
Elevator2.dispatch() | 58.0 | 1.0 | 10.0 | 25.0 |
Schedule2.gettwoscale(char, char) | 37.0 | 1.0 | 9.0 | 10.0 |
Input.run() | 17.0 | 3.0 | 8.0 | 8.0 |
Schedule1.run() | 14.0 | 3.0 | 9.0 | 9.0 |
Schedule2.run() | 11.0 | 3.0 | 6.0 | 6.0 |
Controller.personreq2(Req) | 10.0 | 5.0 | 8.0 | 8.0 |
Elevator1.parser(ArrayList, ArrayList) | 10.0 | 1.0 | 7.0 | 7.0 |
Elevator2.parser(ArrayList, ArrayList) | 10.0 | 1.0 | 7.0 | 7.0 |
Schedule1.takenable(Req) | 9.0 | 3.0 | 5.0 | 7.0 |
Elevator2.isup(Req) | 8.0 | 4.0 | 2.0 | 4.0 |
Elevator2.isupp(Req) | 8.0 | 4.0 | 2.0 | 4.0 |
Elevator2.isuppp(Req) | 8.0 | 4.0 | 2.0 | 4.0 |
Counter.acquire() | 7.0 | 3.0 | 4.0 | 4.0 |
Elevator1.stay() | 7.0 | 1.0 | 8.0 | 8.0 |
Elevator2.stay() | 7.0 | 1.0 | 8.0 | 8.0 |
Schedule1.available(Req) | 7.0 | 1.0 | 4.0 | 5.0 |
Schedule2.getscale(char, char) | 7.0 | 1.0 | 4.0 | 5.0 |
Elevator1.out() | 5.0 | 1.0 | 4.0 | 4.0 |
Elevator2.out() | 5.0 | 1.0 | 4.0 | 4.0 |
Schedule1.setElevator(ArrayList) | 5.0 | 4.0 | 4.0 | 4.0 |
Elevator1.in() | 3.0 | 3.0 | 2.0 | 3.0 |
Elevator1.run() | 3.0 | 3.0 | 9.0 | 10.0 |
Elevator2.in() | 3.0 | 3.0 | 2.0 | 3.0 |
Elevator2.run() | 3.0 | 3.0 | 9.0 | 10.0 |
Schedule1.latest(Req) | 3.0 | 1.0 | 2.0 | 3.0 |
Controller.alloc(Req) | 2.0 | 1.0 | 2.0 | 2.0 |
Controller.init() | 2.0 | 1.0 | 3.0 | 3.0 |
Controller.setend() | 2.0 | 1.0 | 3.0 | 3.0 |
Elevator1.getDirection() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator1.getTheState() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator2.down() | 2.0 | 1.0 | 2.0 | 3.0 |
Elevator2.getDirection() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator2.getTheState() | 2.0 | 3.0 | 1.0 | 3.0 |
Elevator2.up() | 2.0 | 1.0 | 2.0 | 3.0 |
Controller.elevatorreq1(ElevatorRequest) | 1.0 | 1.0 | 6.0 | 6.0 |
Controller.elevatorreq2(ElevatorRequest) | 1.0 | 1.0 | 11.0 | 11.0 |
Controller.personreq1(Req) | 1.0 | 1.0 | 6.0 | 6.0 |
Elevator1.close() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator1.down() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator1.open() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator1.up() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator2.close() | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator2.getreachable(char) | 1.0 | 2.0 | 1.0 | 2.0 |
Elevator2.open() | 1.0 | 1.0 | 2.0 | 2.0 |
Schedule1.availElevatorIndex(Req) | 1.0 | 2.0 | 1.0 | 2.0 |
Schedule2.Schedule2(int) | 1.0 | 1.0 | 2.0 | 2.0 |
Controller.getInstance() | 0.0 | 1.0 | 1.0 | 1.0 |
Counter.Counter() | 0.0 | 1.0 | 1.0 | 1.0 |
Counter.getInstance() | 0.0 | 1.0 | 1.0 | 1.0 |
Counter.release() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator1.Elevator1(Queue, int, int, int, char, int, long, ...) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator1.getNowFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator1.getPeople() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator1.getSize() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator1.getSpeed() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.Elevator2(Queue, int, int, int, char, int, long, ...) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.getCurBuilding() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.getPeople() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.getSpeed() | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator2.getmm() | 0.0 | 1.0 | 1.0 | 1.0 |
MainClass.main(String[]) | 0.0 | 1.0 | 1.0 | 1.0 |
Output.Output() | 0.0 | 1.0 | 1.0 | 1.0 |
Output.getInstance() | 0.0 | 1.0 | 1.0 | 1.0 |
Output.println(String) | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.Queue(boolean, ArrayList) | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.getQueueisEnd() | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.getWaitQueue() | 0.0 | 1.0 | 1.0 | 1.0 |
Queue.setQueueisEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Req.Req(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
Req.getFromBuilding() | 0.0 | 1.0 | 1.0 | 1.0 |
Req.getFromFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Req.getPersonId() | 0.0 | 1.0 | 1.0 | 1.0 |
Req.getToBuilding() | 0.0 | 1.0 | 1.0 | 1.0 |
Req.getToFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Req.gettrueToBuilding() | 0.0 | 1.0 | 1.0 | 1.0 |
Req.gettrueToFloor() | 0.0 | 1.0 | 1.0 | 1.0 |
Req.setfrombuilding(char) | 0.0 | 1.0 | 1.0 | 1.0 |
Req.setfromfloor(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Req.settobuilding(char) | 0.0 | 1.0 | 1.0 | 1.0 |
Req.settofloor(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.Schedule1(int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.addElevator(int, double, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.addRequest(Req) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.dispacher() | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule1.setEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.addElevator(int, double, int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.addRequest(Req, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.getdistance(int, int) | 0.0 | 1.0 | 1.0 | 1.0 |
Schedule2.setEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Total | 353.0 | 134.0 | 249.0 | 307.0 |
Average | 3.922222222222222 | 1.488888888888889 | 2.7666666666666666 | 3.411111111111111 |
CogC
=
Cognitive complexity`(认知复杂度)
ev(G)
=Essential cyclomatic complexity
(基本圈复杂度)
iv(G)
=Design complexity
(设计复杂度)
v(G)
=cyclonmatic complexity
(圈复杂度)
可以看到,dispatch方法、gettwoscale方法复杂度较高。
可能原因是电梯调度算法较复杂,横向电梯换乘策略算法较复杂。
其余方法复杂度尚可。
Bug分析
程序设计过程中的bug
程序设计过程中遇到过的bug,多数为细节处理的考虑不周造成的。
比如曾出现有指令还在换乘,但输入NULL,所有电梯停止(指令还没换乘结束)的bug,其原因是对于线程结束的判断出了问题。我参考实验课,引入了Counter类解决了这个问题。
公测中的bug
HW3的公测中,我的程序没有被检测出bug。
互测中的bug
HW3的互测中,我的程序被检测出了2组bug,均为同质bug。bug原因是:当添加电梯指令和乘客请求同时到来时,如果此电梯恰好为该乘客换乘的最佳选择电梯,程序会选择该电梯,但该电梯并未准备好,会出现请求无法执行的bug。
HW3的互测中,我没有hack到其他同学。
测试思路
测试思路是 坑点针对测试+随机测试
其中坑点针对测试需要的是仔细分析指导书,找出指导书中的一些边界情况
针对这些边界情况或者易忽视的情况,构造测试数据进行测试。
随机测试就是随机构造数据,可以自己编写(或白嫖)测试程序。
我的感受
本次作业,我实践了流水设计模式,加深了对于多线程协调与线程安全的理解。
本次作业,看似只是多了可达性的问题,但由于其复杂性,整个电梯系统的设计难度都大大增加了,换乘所带来的请求多次出入电梯,也带来了许多线程安全隐患与程序设计障碍。其中一些不易察觉的bug,更是在课下自查中没有查出,而在互测中被暴露了出来。
对第二单元的感受
第二单元通过3次迭代开发的作业,让我体会到了多线程程序设计的思想特质、让我学会了多线程的基础知识和编程方法,熟悉了同步块与锁、线程安全设计、多线程设计模式等内容。
作业难度不小、颇具挑战,多线程的不确定性、一些bug的难以复现性更是为我带来了不小的困难。在线程安全设计,并发及其协调设计、并发场景的需求分析与设计等方面我还需要继续加强。
3次作业,我在公测中没有被查出任何bug,在互测中HW1被查出1个bug,HW3被查出1个bug。总体而言还是满意的,bug较少也证明了我在课下程序设计与测试中的付出没有白费。
今后课程的挑战会更多,我期望学习更多,收获更多。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步