BUAA_OO_Unit2总结
BUAA_OO_Unit2总结
一、总述
在第二单元的学习中,我们学习了多线程的相关知识,了解了线程安全问题的解决办法,并在三次作业的迭代开发过程中建立了一个功能不断丰富的电梯系统。
最终的UML类图如下:
最终的时序图如下:
二、作业分析
2.1 第五次作业
2.1.1 作业要求
用多线程的方式实现线程安全的电梯,保证电梯仅在五个都做中每栋初始存在一个,保证每个请求的出发楼座和终止楼座相同,并要求总运行时间最短。
2.1.2 具体实现
第一次接触多线程遇到了不少的困难,一开始尝试的时候没有使用共享队列,对加锁的具体语法也不甚了解(当时甚至不知道可以对单个对象加锁),导致自己的初版电梯仅能在一条请求处理后才能读入新的请求。
在获得实验样例代码后,对自己的输入线程和控制线程进行了完全的重构,最终有效解决了上述的问题,最终的解决方法如下:
Main类创建和启动InputHander类和Schedule类,读入线程负责和官方读入包接口,调度器负责创建电梯和将请求传入电梯等待序列,电梯通过等待序列和自身内部序列来决定自身的运行逻辑。
在输入结束时,End信号由输入线程传递给调度器,再由调度器传递给电梯,从而结束电梯的运行、
2.1.3 调度策略
第五次作业由于每座仅存在一个电梯,因此调度策略仅存在单电梯的调度策略,在总结前人的经验的基础上,没有使用基准策略的ALS算法,而是使用了简单高效的look算法。
具体的运行逻辑也十分简单:
1.当电梯内存在乘客或运行方向存在请求时,保持原方向不变。
2.当电梯内不存在乘客且运行方向无请求时,若反方向存在请求,改变运行方向。
3.若请求序列和乘客序列均为空,电梯线程wait。
接人逻辑:
若电梯未满且请求方向和电梯运行方向相同,则将该乘客接入电梯。
2.1.4 线程安全
对输出类加上synchronized锁来保证输出时间戳单调递增;
由于单电梯所以等待序列不需要额外上锁。
2.1.5 复杂度分析
一些主要方法的复杂度如下:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Controller.wake() | 1.0 | 1.0 | 2.0 | 2.0 |
Main.getRequest(int) | 1.0 | 2.0 | 1.0 | 2.0 |
Main.main(String[]) | 1.0 | 1.0 | 2.0 | 2.0 |
Elevator.move() | 3.0 | 1.0 | 4.0 | 4.0 |
Elevator.look() | 4.0 | 1.0 | 4.0 | 4.0 |
InputThread.run() | 5.0 | 3.0 | 4.0 | 4.0 |
RequestQueue.getOneRequest() | 5.0 | 2.0 | 4.0 | 5.0 |
Elevator.open() | 6.0 | 1.0 | 6.0 | 6.0 |
Schedule.run() | 9.0 | 4.0 | 5.0 | 6.0 |
Elevator.isOpen() | 17.0 | 8.0 | 9.0 | 12.0 |
Elevator.personIn() | 20.0 | 6.0 | 8.0 | 10.0 |
Elevator.run() | 25.0 | 3.0 | 19.0 | 20.0 |
Elevator.getDirection() | 36.0 | 15.0 | 17.0 | 23.0 |
Total | 133.0 | 65.0 | 102.0 | 117.0 |
Average | 4.290322580645161 | 2.1666666666666665 | 3.4 | 3.9 |
总体而言,复杂度并不算太高,主要的高复杂度方法都集中在电梯决策和线程的运行函数上。
2.1.6 不足和失误
1.顶层逻辑扩展性极差,导致第六次作业主要的工作变成顶层逻辑的重构。
2.wait()和notifyAll()不是很会使用,第五次作业电梯停下来后强行让电梯反复sleep(50)避免轮询。。。(捂脸
3.优化时忘记考虑正确性,优化后的策略导致了极少数情况下的死循(后面细谈
2.2 第六次作业
2.2.1 作业要求
在第五次作业的基础上,增加了新增电梯的请求,同时增加了横向的电梯和请求,数据保证每层和每座的电梯不超过3,且每个乘客请求的起始楼座和终止楼座、起始楼层和终止楼层一定有一个相同,在保证正确性的前提下,要求总的运行时间最短。
2.2.2具体实现
输入线程InputHander类主要用于和官方读入包接口,读取请求并将请求传递给调度器,同时在读入完成时及时传递setEnd信号,实现调度器和电梯线程的终止。
调度器负责对读入请求的识别和传递,先检测读入信号的种类,若为乘客请求,则分配给对应的某些电梯、横向电梯的共享队列,若为新增电梯请求,则建立对应的电梯线程。
电梯类按照自己的运行策略实现乘客的接取、方向选择。
具体储存结构:
电梯:以Arraylist<Arraylist
共享序列:根据调度策略的不同,选择不同的储存结构。
自由竞争:以Arraylist<Arraylist
预先分配:以Arraylist<Arraylist<ArrayList
最终第六次作业的整体实现结构如下:
2.2.3 调度策略
在单电梯的运行逻辑上,依然沿用第五次作业中的look策略。
在多电梯的协作方面,为了选择最优的调度策略,本次作业使用了两种不同的调度策略进行比对。
自由竞争:每个电梯共用同一个共享队列,均采用单电梯运行逻辑,当一个电梯尝试上人时对共享对象上锁,“自由竞争”,不对每个电梯如何运行进行人为的干预。
预先分配:每个电梯采用独立的共享队列,每读入一个请求时依靠调度器的逻辑分配到某指定的电梯的共享队列中,每个电梯仅负责自身的共享队列。鉴于本次作业的性能要求,采用了预估电梯运行时间的策略,即:预估每个电梯跑完自身共享序列的时间,每次加入请求时选择预计运行时间最短的电梯的请求序列加入。
以强测数据为例,二者运行结果如下:
可见,在大多数情况下,二者运行效率相同,但随着同座单位时间投入请求增多,预先分配的效率显著下降。因此,最后选择了自由竞争策略进行了提交。
总体来看,二者各有优劣:
自由竞争 | 预先分配 |
---|---|
优势:1.代码简单,好写好调;2.面对大量随机数据时效率显著。 | 优势:1.小数据时效果好(局部最优解);2.可控性好,选择不同的分配策略可以针对不同的性能要求。 |
劣势:1.存在陪跑现象;2.随机性强,实际运行逻辑不可控。 | 劣势:1.局部最优解在全局看来可能很差;2.预估运行时间函数编写相对困难。 |
2.2.4 线程安全
对于共享对象上锁问题:对电梯请求序列的add()和remove()操作都需要上锁,遍历共享序列时没有上锁但强测和互测也没有出问题(但是在第七次作业时又觉得应该上锁
输出类的锁与上次作业基本相同。
需要一个合适的wait()逻辑避免产生轮询问题。
2.2.5 复杂度分析
部分主要的方法的复杂度如下表:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
TransverseElevator.insert(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
TransverseElevator.push(PersonRequest) | 0.0 | 1.0 | 1.0 | 1.0 |
TransverseElevator.setEnd(boolean) | 0.0 | 1.0 | 1.0 | 1.0 |
Elevator.clone(ArrayList) | 1.0 | 1.0 | 2.0 | 2.0 |
Schedule.addElevator(ElevatorRequest) | 2.0 | 1.0 | 3.0 | 3.0 |
Schedule.Schedule(RequestQueue) | 3.0 | 1.0 | 4.0 | 4.0 |
Elevator.look() | 4.0 | 1.0 | 4.0 | 4.0 |
TransverseElevator.look() | 4.0 | 1.0 | 4.0 | 4.0 |
TransverseElevator.move() | 4.0 | 2.0 | 4.0 | 5.0 |
Elevator.move() | 5.0 | 2.0 | 5.0 | 6.0 |
Elevator.open() | 5.0 | 1.0 | 5.0 | 5.0 |
RequestQueue.getOneRequest() | 5.0 | 2.0 | 4.0 | 5.0 |
TransverseElevator.getDirection() | 5.0 | 2.0 | 3.0 | 5.0 |
TransverseElevator.open() | 5.0 | 1.0 | 5.0 | 5.0 |
Schedule.addpeople(PersonRequest) | 7.0 | 1.0 | 5.0 | 5.0 |
TransverseElevator.isOpen() | 7.0 | 5.0 | 4.0 | 6.0 |
InputThread.run() | 9.0 | 3.0 | 4.0 | 6.0 |
TransverseElevator.personIn() | 12.0 | 3.0 | 4.0 | 6.0 |
Elevator.isOpen() | 17.0 | 8.0 | 9.0 | 12.0 |
Elevator.personIn() | 20.0 | 6.0 | 8.0 | 10.0 |
Elevator.run() | 23.0 | 3.0 | 16.0 | 17.0 |
TransverseElevator.run() | 23.0 | 3.0 | 16.0 | 17.0 |
Schedule.run() | 34.0 | 4.0 | 13.0 | 14.0 |
Elevator.getDirection() | 36.0 | 15.0 | 17.0 | 23.0 |
Elevator.getTime() | 62.0 | 9.0 | 24.0 | 28.0 |
Total | 293.0 | 96.0 | 184.0 | 213.0 |
Average | 6.659090909090909 | 2.2325581395348837 | 4.27906976744186 | 4.953488372093023 |
可见,预估电梯运行时间的方法(getTime())由于基本要把所有决策重现实现一遍,因此复杂度巨大,但事实上需要的时间可以接受;除此之外,和第五次作业基本相同。
2.2.6 不足和失误
1.横向电梯的运行时间是0.2s!!!(当时完全没注意到,感谢课程组放我一马
2.预先分配策略在加入新电梯时没有重新规划分配序列,由于时间有限,且最终选择了自由竞争,部分功能实现还不够完全。
3.遍历共享序列可能存在一些线程安全问题?(倒确实没测出来
2.3 第七次作业
2.3.1 作业要求
在上两次作业的基础上,增加了横向电梯的可达楼座,增加了电梯的容量和运行速度的个性化定制,同时,乘客请求可以同时跨越楼层和楼座。基础电梯保证了乘客请求的可达性。在保证正确性的前提下,要求总运行时间加上所有请求的等待时间之和最短。
2.3.2 具体实现
整体架构与上次基本相同。
自己继承Request类建立了passenger类代替了原有的PersonRequest类,来表示乘客请求。结构如下:
public class Position {
private int building;
private int floor;
}
public class Passenger extends Request{
private int id;
private Pair nowPosition;
private Pair nextPosition;
private Pair finalPosition;
}
电梯类增加了向调度器增加新的乘客请求的功能。
为了便于与增加电梯的功能契合,乘客请求分配采用动态分配的方式,及每一次仅决定乘客下一步的目的地,之后的运行规划在到达目的地后决定。
2.3.3 调度策略
单电梯策略依旧采用look策略;
多电梯协作采用自由竞争策略;
对于每步目的地的规划,采用预估时间最短路的方式;
具体的实现方法为:
将五座十层电梯视为5×10的网格图,将纵向电梯/横向电梯视为同列/同行间的\(k×(k-1)/2\)条边(k为电梯可达楼层/楼座数),边权为电梯实际运行时间t1+预估等待时间t2,那么每个请求可转化成时间为权值的最短路问题。
对于每层每座多电梯的情况,设两点之间可达电梯数为k,则该边的边权\(dis=\sum_{i = 1}^{k}{t1_i/k+t2_i/k^2}+0.4\)
具体的t2预估方式如下:
纵向电梯:由于请求数据量较大时look策略下实际效率与scan效率近似,可以简单比对scan策略下等待时间,易知,scan策略下电梯有18种状态(一层↑,十层↓,其余层↑↓),且每种状态出现概率相等,对任意层任意方向请求,十八种状态等待时间为0,1,2.....17次arrive间隔,总等待时间期望时长为:(0+17)/2*v=8.5v(v为电梯单位楼层运行时长)。
横向电梯:采用同样的预估方式,得到的预期等待时长为2v。
由于最短路策略逻辑简单,并不需要具体判断下一步该横向还是纵向移动,而且最短路代码逻辑非常简单(最短路floyd不超过十行,\(next[i][j]\)表示以i为起点j为终点的最短路的下一个目的地可以直接在floyd中处理),因此码长和基准策略相比甚至更短,逻辑也更不容易出错。而且在t2预估准确的情况下,性能要比基准策略优秀得多。核心代码如下:
for (int k = 1; k <= 50; k++) {
for (int i = 1; i <= 50; i++) {
for (int j = 1; j <= 50; j++) {
if (k != j && k != i && i != j) {
if (dis[i][k] + dis[k][j] < dis[i][j]) {
dis[i][j] = dis[i][k] + dis[k][j];
next[i][j] = next[i][k];
}
}
}
}
}
2.3.4 线程安全
由于电梯类可以向调度器传递请求,需要对setEnd逻辑进行处理,在调度器中加入一个计数器,每新增一名乘客计数器+1,每当一名乘客到达终点计数器-1,当输入类End置1且计数器归零结束线程;
由于横向电梯有不可到达楼层,更改wait()逻辑以避免横向电梯空等轮询;
2.3.5 复杂度分析
部分复杂度较高的方法的复杂度如下:
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
Passenger.equals(Object) | 3.0 | 3.0 | 4.0 | 6.0 |
TransverseElevator.canRun() | 3.0 | 3.0 | 2.0 | 3.0 |
Elevator.look() | 4.0 | 1.0 | 4.0 | 4.0 |
Position.equals(Object) | 4.0 | 3.0 | 2.0 | 5.0 |
Schedule.addpeople(Passenger) | 4.0 | 2.0 | 4.0 | 5.0 |
TransverseElevator.look() | 4.0 | 1.0 | 4.0 | 4.0 |
TransverseElevator.move() | 4.0 | 2.0 | 4.0 | 5.0 |
Elevator.move() | 5.0 | 2.0 | 5.0 | 6.0 |
Elevator.open() | 5.0 | 1.0 | 5.0 | 5.0 |
RequestQueue.getOneRequest() | 5.0 | 2.0 | 4.0 | 5.0 |
TransverseElevator.open() | 5.0 | 1.0 | 5.0 | 5.0 |
TransverseElevator.isOpen() | 8.0 | 6.0 | 5.0 | 8.0 |
InputThread.run() | 9.0 | 3.0 | 4.0 | 6.0 |
TransverseElevator.getDirection() | 10.0 | 5.0 | 5.0 | 9.0 |
Schedule.Schedule(RequestQueue) | 12.0 | 1.0 | 9.0 | 9.0 |
TransverseElevator.personIn() | 13.0 | 3.0 | 5.0 | 7.0 |
Elevator.isOpen() | 17.0 | 8.0 | 9.0 | 12.0 |
Schedule.addElevator(ElevatorRequest) | 17.0 | 1.0 | 8.0 | 9.0 |
Elevator.personIn() | 20.0 | 6.0 | 8.0 | 10.0 |
Elevator.run() | 23.0 | 3.0 | 16.0 | 17.0 |
Schedule.calculation() | 23.0 | 1.0 | 1.0 | 11.0 |
TransverseElevator.run() | 23.0 | 3.0 | 15.0 | 16.0 |
Schedule.run() | 33.0 | 4.0 | 14.0 | 15.0 |
Elevator.getDirection() | 36.0 | 15.0 | 17.0 | 23.0 |
Total | 292.0 | 127.0 | 207.0 | 253.0 |
Average | 4.055555555555555 | 1.7887323943661972 | 2.915492957746479 | 3.563380281690141 |
复杂度与上次作业相比基本相同,总体而言可以接受。
三、互测和性能分析
3.1第五次作业
自己:因为瞎优化接人逻辑,导致在极少数情况下电梯会反复横跳,然后寄了三个点(真的是给自己一个教训时刻应该以正确性为第一优先级。
互测:输出线程不安全一直hack不到人,然后一直在交,一直在交。最后截止后重测发现自己hack了同质bug超多次,不是故意的,捂脸。
3.2 第六次作业
题外话:早上起床发现saber1刀6,下午发现横向电梯可以0.4s/层真的以为自己进了c房。。。
自己:强测和互测均无bug
互测:1.一个同房电梯会超载
2.一个电梯会rlte停不下来
3.3 第七次作业
自己:强测和互测均无bug
互测:1.一个电梯会rtle
2.一个电梯会接到不该接的人
3.4 数据构造
实际上由于这两周相对较忙,互测数据大多数以满的随机数据为主,偶尔会构造一些极端数据,轮询的数据一直不怎么会构造,后两次房里都有也没有卡到人。
3.5 性能分析
对性能还算满意,第五次作业凉也并不是性能的锅,事实上如果没写出bug应该也能有98分左右。
第六次作业性能分98.8其实不是特别满意,不少优化感觉都做了无用功,分析自己比别人慢的原因貌似是某些步快了导致接近时间的请求没有接上所以反而慢了不少?(其实个人感觉以总等待时间计算性能分会比电梯运行时间合理的多)
第七次作业99.8的性能分还算在预期内,毕竟预期最短路的效率客观上会比基准策略快上不少,扣掉的分数可能是因为自己的预估时间过于保守(按照scan估算look的等待时间)导致部分决策没有达到最优解,由于是最后一次作业,也没在课下继续调参。
四、总结
这个单元加深了自己对多线程的理解,也深刻地告诫了自己要把正确性放在性能之前(大哭),还有和同学对拍的重要性。
同时,对面向对象思想的理解也更加透彻,希望能在以后的学习过程中继续提高自己的面向对象思维和编程能力。