BUAA OO 第二单元总结
一、架构简述
这一单元的作业在最初时就尽可能考虑到后面的扩展性问题(尤其是在经历了第一单元的摧残之后),因此基本没有重构。首先分析一下这一单元采用的整体架构,方便下面几项分析的理解。
第一次作业
第一次作业仅涉及到一部电梯的模拟运行,因此没有调度器的设计。整体设计主要包括输入设备、等待队列和电梯三部分,采用生产者消费者模式。输入设备为生产者,电梯为消费者,托盘为等待队列。输入设备仅负责接收标准输入并把解析好的请求传送到等待队列中,电梯负责从等待队列里面取出请求并完成这一请求。电梯的运行采用look算法,在每次状态发生变化后重新设置最远目标点,并一直向最远目标点移动,每当抵达某一层,当电梯内人未满,且该层有与电梯同方向的请求,则进行捎带。架构相对比较简单,但效果很好。
第二次作业
第二次作业在第一次作业的基础上增加了调度器,并为每一个电梯设置了等待队列,调度器的任务是将总等待队列里面的请求取出来平均地放到各电梯的请求队列中。
第三次作业
第三次作业在第二次作业的基础上对电梯进行了分类。考虑到换乘操作的复杂性和较高的时间成本,本次作业未采用换乘操作,调度器的任务是对请求进行分类,分别按照一定的权重分配到不同种类的电梯的等待队列中。
二、同步块与锁的设置与选择
第一次作业
同步块和锁是本单元的主要内容之一,在第一次作业中,为了尽可能降低锁造成的延时,采用了尽可能锁小段代码的思路。
第一次作业中,我的同步对象主要为等待队列,任何读写等待队列的行为均需要加锁。同时,由于需要判断电梯是否需要结束,设置了一个noFutureRequest变量,在读写这一个变量时需对电梯加锁。
但是在这一次作业中将该变量放在了电梯类中,导致出现了加两把锁的情况,很有可能出现死锁。
synchronized (this) {
synchronized (requestWaitMap) {
if (noFutureRequest) {
break;
} else if (requestWaitMap.noWaitPeople()) {
requestWaitMap.wait();
}
if (noFutureRequest) {
break;
}
}
}
第二次作业
第二次作业的同步对象为总等待队列和电梯内的等待队列,不再将电梯作为同步对象,任何读写同步对象的行为均需要加锁,并遵循在保证线程安全的前提下尽可能锁小段代码以提高效率。
由于发现第一次作业中存在可能死锁的问题,对第一次作业进行了少量的修改。
最初修改时进一步缩小了同步块的代码量,成为下面的模式。
synchronized (this) {
if (noFutureRequest) {
break;
}
}
synchronized (requestWaitMap) {
if (requestWaitMap.noWaitPeople()) {
requestWaitMap.wait();
}
}
synchronized (this) {
if (noFutureRequest) {
break;
}
}
但是此种情况下很有可能出现线程安全问题,如在第一次判断noFutureRequest后将noFutureRequest置为true,就会导致电梯一直处于wait状态,从而无法结束线程。
经过分析后,发现noFutureRequest设置的条件是输入为null,而这一部分的输入应与其他输入是平行关系,因此将noFutureRequest放置在电梯类中就不是很明智。在本次作业中,微调了此变量的位置,将其放置在各电梯的等待队列中,更新后的代码如下,有效解决了可能的死锁和线程安全问题。
public synchronized boolean newPassenger() throws InterruptedException {
while (true) {
if (waitNum == 0) {
if (noFutureRequest) {
return false;
} else {
this.wait();
}
} else {
return true;
}
}
}
第三次作业
第三次作业仅仅修改了调度器的调度策略(从平均分配到按类平均分配),因此进程块和锁的设置并没有大变化。
三、调度器设计
第一次作业
第一次作业仅仅涉及到一部电梯,因此没有涉及调度器,电梯直接从输入设备中获取数据。
第二次作业
第二次作业开始设计调度器,设计过程中将调度器设计为一个调度器调度所有电梯,所以设计调度器时采用了单例模式。
调度器的任务是从总的等待队列里面获取请求,并把请求平均分配到各个电梯独自的等待队列中,并在没有新请求时将各电梯等待队列中的标识位置为true。
线程交互方面,本次作业有三个线程,输入线程,调度器线程和电梯线程。调度器线程与输入线程的交互式通过共享对象总等待队列实现的。输入线程向总等待队列写入新请求,调度器线程则从总等待队列获取新的请求。调度器线程与电梯线程的交互是通过各个电梯独自的等待队列这一共享对象实现的。调度器向这些等待队列中输入请求,而各个电梯从这些等待队列中获取并处理请求。
第三次作业
第三次作业的调度器与第二次相比并无大的变化,仅仅将总等待队列的请求进行分类后按类平均分配到各个电梯的请求队列中。
四、可扩展性分析
可扩展性分析主要分析第三次作业,因为前两次作业到第三次作业的过程中基本没有进行重构,所以第三次作业更具有代表性。
UML图

ULM顺序图
作业共涉及到四个线程,其中MainClass类的线程为主线程,但实际上只负责创建其他三个线程,并不进行任何操作便结束。
顺序图如下。
MainClass类线程

PassengerReceiver类线程

Handler类线程
Elevator类线程

复杂度分析
方法复杂度分析
| method | CogC | ev(G) | iv(G) | v(G) |
|---|---|---|---|---|
| Elevator.arrived() | 0 | 1 | 1 | 1 |
| Elevator.closeDoor() | 0 | 1 | 1 | 1 |
| Elevator.getElevatorWaitMap() | 0 | 1 | 1 | 1 |
| Elevator.getKind() | 0 | 1 | 1 | 1 |
| Elevator.openDoor() | 0 | 1 | 1 | 1 |
| ElevatorWaitMap.noWaitPeople() | 0 | 1 | 1 | 1 |
| ElevatorWaitMap.setNoFutureRequest() | 0 | 1 | 1 | 1 |
| Handler.Handler() | 0 | 1 | 1 | 1 |
| Handler.getInstance() | 0 | 1 | 1 | 1 |
| Handler.putIn(Passenger,ArrayList,int) | 0 | 1 | 1 | 1 |
| MainClass.main(String[]) | 0 | 1 | 1 | 1 |
| Passenger.Passenger(int,int,int) | 0 | 1 | 1 | 1 |
| Passenger.getFrom() | 0 | 1 | 1 | 1 |
| Passenger.getId() | 0 | 1 | 1 | 1 |
| Passenger.getTo() | 0 | 1 | 1 | 1 |
| PassengerReceiver.PassengerReceiver(ElevatorInput) | 0 | 1 | 1 | 1 |
| PassengerWaitMap.PassengerWaitMap() | 0 | 1 | 1 | 1 |
| PassengerWaitMap.addPerson(Passenger) | 0 | 1 | 1 | 1 |
| PassengerWaitMap.getInstance() | 0 | 1 | 1 | 1 |
| PassengerWaitMap.getPassenger() | 0 | 1 | 1 | 1 |
| PassengerWaitMap.noFutureRequest() | 0 | 1 | 1 | 1 |
| Elevator.Elevator(int,int,int,String,int,int,int) | 1 | 1 | 2 | 2 |
| Elevator.peopleOut() | 1 | 1 | 2 | 2 |
| ElevatorWaitMap.ElevatorWaitMap(int,int) | 1 | 1 | 2 | 2 |
| ElevatorWaitMap.noPassengerMorning() | 1 | 2 | 1 | 2 |
| Handler.stop(ArrayList) | 1 | 1 | 2 | 2 |
| ElevatorWaitMap.addPerson(Passenger) | 2 | 1 | 2 | 2 |
| ElevatorWaitMap.getPassenger(int,boolean) | 2 | 2 | 2 | 2 |
| ElevatorWaitMap.noPassengerHere(int,boolean) | 2 | 2 | 2 | 2 |
| Handler.addElevator(int,int) | 3 | 1 | 3 | 3 |
| ElevatorWaitMap.getHighestRequest(int) | 5 | 4 | 2 | 4 |
| ElevatorWaitMap.getLowestRequest(int) | 5 | 4 | 2 | 4 |
| Handler.setHandler(String,int,int,int) | 5 | 1 | 4 | 4 |
| Elevator.run() | 7 | 1 | 4 | 7 |
| Elevator.peopleIn() | 8 | 3 | 6 | 7 |
| Elevator.setFurthestDes() | 8 | 1 | 4 | 4 |
| ElevatorWaitMap.newPassenger() | 8 | 4 | 4 | 4 |
| PassengerWaitMap.newRequest() | 8 | 4 | 4 | 4 |
| Elevator.changeFloor() | 11 | 5 | 3 | 7 |
| Elevator.nightProcess() | 12 | 6 | 6 | 7 |
| Elevator.randomProcess() | 13 | 4 | 6 | 6 |
| Handler.run() | 15 | 3 | 6 | 10 |
| PassengerReceiver.run() | 15 | 3 | 7 | 8 |
| Elevator.morningProcess() | 26 | 9 | 7 | 10 |
| Total | 160 | 85 | 104 | 126 |
| Average | 3.64 | 1.93 | 2.36 | 2.86 |
从上面的表格可以看出,大部分方法的复杂度均不是很高且处于一个比较平均的水平。但其中morningProcess()方法的CogC比较高,这主要是由于morning模式的特殊性,由于morning模式与另外两个模式的差异性较大,因此采取了比较多的特殊判断,因而造成了CogC大幅提高。总的来看,基本满足高内聚低耦合的特点。
类复杂度分析
| class | OCavg | OCmax | WMC |
|---|---|---|---|
| Elevator | 3.64 | 9 | 51 |
| ElevatorWaitMap | 2.4 | 4 | 24 |
| Handler | 2.43 | 5 | 17 |
| MainClass | 1 | 1 | 1 |
| Passenger | 1 | 1 | 4 |
| PassengerReceiver | 4 | 7 | 8 |
| PassengerWaitMap | 1.5 | 4 | 9 |
| Total | 114 | ||
| Average | 2.59 | 4.43 | 16.29 |
通过类复杂度分析可以看出,复杂度比较高的是电梯类和数据输入类(数据输入类不敢解释呜呜呜),电梯类的复杂度高主要是由于电梯类完成了比较多的工作,包括自己运行状态的改变,对外界条件变化的判断等等。因此,在电梯类这一方面可以将电梯类再拆分成几个功能较为单一的部分,但是由于时间限制的原因并没有实现,在这一点可以进一步优化。
可扩展性分析
通过上面的分析,可以看出本次作业还是基本具备高内聚低耦合的特点,在大部分地方的可扩展性还是比较好的。
由于本次作业采用的是调度器分配人员的至电梯等待序列的方法,因此对于正常电梯在不同条件下运行的情况以及电梯种类的不同均具有比较好的兼容性,最直观的想法即直接更改调度器的分配策略即可实现。
但是由于电梯类书写的比较臃肿,所以对于电梯运行模式的更改可能不具有比较好的扩展性,比如如果令电梯每上升三层就会往下滑一层(建议加到明年的OO作业里面),在本次作业书写的代码中就可能会导致电梯类的重构问题。
五、自己的bug
本次作业在强测和互测中均没有被找到bug,但需要分析自己提交前的线下测试中发现的bug。
1.螺旋升天+花式入土
这种问题主要是出现在类似于下面这样的样例中。
[0.0]Random
[1.0]1-FROM-4-TO-3
[5.0]2-FROM-3-TO-4
这个样例与我设置的电梯运行模式有关,以向上运行为例,我设置的电梯运行模式为获取具有请求的最高楼层作为最远目标点,每进入一个乘客,将该乘客目的地与目标点比较,若目的地更远,则更新目标点为目的地。当电梯抵达最远目标点时,再重复搜索具有请求的最高楼层(因为可能运行过程中又来了新的乘客),若比当前楼层低则更改方向,若无请求,则进入wait()状态,被唤醒时重复上面的操作。
在这个过程中涉及到出bug的方法有两个,更改楼层changeFloor()以及寻找最高的具有请求的楼层getHighestRequest()。
private void changeFloor() throws InterruptedException {
while (true) {
if (!passengerProcessMap.get(nowFloor).isEmpty()) {
break;
}
if (peopleNum < fullNum && !elevatorWaitMap.noPassengerHere(nowFloor, up)) {
break;
}
if (nowFloor == furthestDes) {
break;
}
if (up) {
nowFloor++;
} else {
nowFloor--;
}
Thread.sleep(moveTime);
this.arrived();
}
}
public int getHighestRequest(int nowFloor) {
for (int i = highestFloor; i >= nowFloor; --i) {
synchronized (passengerWaitMapUp.get(i)) {
if (!passengerWaitMapUp.get(i).isEmpty()) {
return i;
//return i + 1;
}
}
synchronized (passengerWaitMapDown.get(i)) {
if (!passengerWaitMapDown.get(i).isEmpty()) {
return i;
}
}
}
return nowFloor;
}
修改bug前,在changeFloor()方法中,nowFloor变量的更改在三个break之前,在getHighestRequest()方法中使用的为注释上面的那条语句。这就会导致电梯停在3楼被唤醒后获得最远目的地为3楼,而此时电梯并没有换向,在changeFloor()方法执行后变为2楼,永远无法达到跳出循环的条件完美上演花式入土。螺旋升天与此类似。
修改这个bug主要的思路是首先在changeFloor()之前先判断是否为极限条件,避免无法跳出循环的情况的出现,其次对获取最远目的地进行巧妙处理,同向同层的请求在相同方向上顺延一层(毕竟没有那种在这一层进了电梯接着出来的怪人应该吧),就可以解决螺旋升天+花式入土的bug。
2.永动机
呃换句话说就是这个电梯的线程结束不了,实际上就是前面同步块那一节中讲述的线程安全问题,在经过了条件判断后进入了本应进行判断的那个变量导致的线程进入wait()无法被唤醒。
解决的思路很简单,就把与这些有关的语句块全部用sychonrized加锁即可,主要的问题是分析线程安全的过程。
分析线程安全可以尝试在一个线程的任何非原子操作的地方添加一些其他线程的奇奇怪怪的操作,判断一下会不会引起自己程序的崩溃,同时也提醒我们与某个共享对象有关的所有操作都应该在一个锁里面进行(实际上就是你永远不知道别人会对你的半成品做出什么奇怪的事情)。
3.定点爆破
这个……主要是自刀……针对自己的算法手动构造了样例,因为我的分配方法是通过调度器分类后均分,即A+B均分给A、B,A+C均分给A、C,但是由于A类电梯运行比较慢,所以可以采用极限数据定点爆破(不过互测里好像不允许这种数据?)。模仿第二次强测在一定时间后进行输入,令B、C类电梯搭载路程较短的人,而令A类电梯总搭乘1-20楼或1-19楼这种极限数据,因为1-20楼可由C类搭乘,1-19楼可由B类搭乘,速度显然要比A快得多得多,而且在此种分配策略下,若A、B、C类数据平均化,A类电梯等待队列期望人数约为B、C的3倍,显然会使的RTLE。
这个问题好像需要更改调度策略,并没有找到比较好的解决措施。
六、寻找别人bug的措施
说实在的本次作业找别人bug是真的难……
但是,一个人都没有hack到呜呜呜……我尽力了……
七、心得体会
1.线程安全与效率
这两个好像有一点矛盾,因为线程安全是希望尽可能的大范围锁住代码块,最好让一个线程干完活之后再让另一个线程再干活(我只锁教室另一个人K歌还会影响到我,但我把新主楼锁住了我就无所畏惧了)。但是效率期望的是尽量减小锁的数量和锁的代码块大小,因为在同样的空间里,并发的程度越高CPU利用率越高,就会使效率越高。因此需要尽可能的平衡二者之间的关系,以线程安全为主,在保证安全的基础上追求效率。(毕竟炸不了的导弹飞得再快也没用呃……)
2.高内聚低耦合
这一点还是要不断的强调,因为在奇怪的甲方的压迫下面向对象的思想个人认为重点就在于这六个字。我们永远无法精确预测到未来会遇到什么稀奇古怪的需求,只能尽可能考虑全面,让自己的程序强大一点,多功能一点。
这一周,老师所讲的传入方法的参数的问题,宏观的父类要比微观的子类更加方便。仔细思考一下的确如此,宏观的父类作为参数传入方法,我们可以在父类的基础上再设计子类来面对未来不同的需求,但是如果传入的是子类,那……未来可期——凉凉。
3.关于死锁
拒绝复杂操作从我做起,从现在做起。

浙公网安备 33010602011771号