BUAA OO第二单元总结
BUAA OO第二单元总结
1.基本思路
设置InputThread
类,用来处理并分发请求至对应的楼座队列,该线程在所有输入都完成分配之后,会将每个楼座的候乘表的isEnd
属性置为true,通知其输入结束。控制器负责管理楼座中的电梯与候乘表,而电梯作为一个线程独立运行。电梯的具体行为由策略类给出,每执行完一个动作都要切换一次状态,直至电梯将所有乘客送达且无新乘客,电梯线程退出循环,run函数执行完毕。
2.设计架构
1)UML类图
2)同步块与锁的设计
①本次设计中只出现了对RequestQueue
加锁的情况,因为它相当于生产者消费者模式中的托盘,生产者线程InputThread
在得到了一个新的Request时,会向RequestQueue
中放入此请求;而消费者线程Elevator则会在开门上乘客的时候从RequestQueue
中取出新的请求,显然这些操作是必须要做到互斥的。因此所有的涉及RequestQueue
的方法都是需要用synchronized修饰的。
②最主要的需要加锁的方法为add与putIntoElevator:
Ⅰ add用于实现向RequestQueue
中加入请求
public synchronized void add(PersonRequest request) throws InterruptedException {
Floor floor = this.getFloors().get(request.getFromFloor() - 1);
if (request.getFromFloor() > request.getToFloor()) {
floor.getDownQueue().add(request);
} else {
floor.getUpQueue().put(request);
}
notifyAll();
}
Ⅱ putIntoElevator用于实现从RequestQueue
中取出请求,放入电梯的HashMap
中。
public synchronized Integer putIntoElevator(
HashMap<Integer,LinkedBlockingQueue<PersonRequest>> passengerRequest,
Integer curNum, Integer curFloor, boolean up) throws InterruptedException {
int putNum;
if (up) {
LinkedBlockingQueue<PersonRequest> upQueue =
this.getFloors().get(curFloor - 1).getUpQueue();
putNum = putIn(passengerRequest, curNum, upQueue);
notifyAll();
return putNum;
} else {
LinkedBlockingQueue<PersonRequest> downQueue =
this.getFloors().get(curFloor - 1).getDownQueue();
putNum = putIn(passengerRequest, curNum, downQueue);
notifyAll();
return putNum;
}
}
③其余还有一些getter方法和setter方法也需要加锁,总的来说就是RequestQueue
中的所有方法都要加锁,除非是已经加锁的方法中才调用的private方法(putIntoElevator调用的putIn方法就是例子)。
private Integer putIn(HashMap<Integer,
LinkedBlockingQueue<PersonRequest>> passengerRequest, Integer curNum,
LinkedBlockingQueue<PersonRequest> upQueue) throws InterruptedException {
int upSize = upQueue.size();
if (curNum + upSize >= 6) {
for (int i = 0; i < (6 - curNum); i++) {
PersonRequest toRemove = upQueue.take();
passengerRequest.get(toRemove.getToFloor()).put(toRemove);
printPutInformation(toRemove);
}
return 6 - curNum;
} else {
for (int i = 0; i < upSize; i++) {
PersonRequest toRemove = upQueue.take();
passengerRequest.get(toRemove.getToFloor()).put(toRemove);
printPutInformation(toRemove);
}
return upSize;
}
}
④所有的加锁的方法,都要在return或结束前调用notifyAll()
方法来通知其他的等待线程。
⑤还有一处用到synchronized块的地方,就是电梯线程等待的实现:
private void behave() throws InterruptedException {
switch (status) {
case Stop:
synchronized (requestQueue) {
requestQueue.wait();
}
break;
case Open:
open();
break;
case Close:
close();
break;
case Move:
move();
break;
default:
break;
}
}
当状态为Stop时,我们要让其wait对应的RequestQueue
,这样当生产者再次放入请求时,调用notifyAll()
方法会唤醒此等待线程,这样就避免了电梯空转,实现了资源的高效利用。
3)关于Controller的设计
关于第五次作业要不要设置调度器,在第一次研讨课的时候有所讨论,两种观点分别是:
不加入Controller
不加入调度器的话,在Main中创建电梯线程,并且直接为其分配对应的RequestQueue
。
加入Controller
对于我的设计而言,调度器的确是可有可无的,不过我为了一些方便的因素,把创建Elevator的任务放在了Controller中,所以对于本次作业加不加调度器其实区别不大。因为它只是一个中间态的东西,有点类似于一个bag,你把要用的东西放在一块,这样要用什么的时候比较好拿。
但是对于下一次作业而言,要加入新的电梯了,这时就需要提供加电梯的方法,有了Controller就可以在它的内部实现加电梯方法,这样封装性会比较好,不然就只能在Main里头写了,这样会显得不那么清晰。
我在Controller设计之初的误区
不过我一开始的设计其实非常愚蠢,当时我将Controller作为一个线程,而电梯只是controller的附庸,电梯里存放乘客数据等信息,并提供一些具体的方法供Controller调用。这时电梯的一举一动都是靠Controller来实现的。但是我想了想就知道这样的设计存在着非常大的问题,一部电梯的时候可以这样做,那么多部电梯呢?这样的设计下,一个Contoller调度多部电梯显然是不可能的,所以必须有多个Controller。而且有个致命问题,就是这样写的话,控制逻辑是很不清晰的。根据状态转移图,我们可以构建出转移的逻辑,在这里状态转换要在Controller中实现,但是其他的事情:执行动作等等也需要它来做,这就会让Controller变得异常庞大,这不太符合低耦合的原则。所以我果断放弃了这个想法,而采用了现在的设计:策略组负责状态转换,电梯负责执行,Controller负责管理候乘表与电梯。
3.调度策略
本次作业的调度策略采用的是Look算法(无主请求):
当前方向向上
①如果楼层为10,转换方向;楼层为1,不换方向。
②如果当前层向上的队列有人,不换方向。
③如果比当前层高的任意一层有人,不换方向。
方向向下
①如果楼层为1,转换方向;楼层为10,不换方向。
②如果当前层向下的队列有人,不换方向。
③如果比当前层低的任意一层有人,不换方向。
捎带与开关门
同方向捎带。当前运行方向下,当前层可稍带的乘客或者有需要在该层下楼的乘客数不为0就开门,等0.4s,先下后上,上下完人后关门。
二、第六次作业
1.基本思路
与第五次作业几乎一样,只需要加入横向的电梯类,横向请求队列类,横向Controller类即可。将所有纵向的实现翻译为横向的,就能实现横向电梯。
2.架构设计
1)横向电梯的实现方法
我用的是没怎么考虑扩展性的方法,就是完全复制一套纵向电梯,并把它们改成横向的实现,就算完成了横向电梯。有区别的地方主要在横向电梯的请求队列以及捎带策略。由于横向电梯是环形运行的,所以它的调度就与纵向电梯不太相同。纵向电梯很明确的有方向性,我要怎么走是有方向性的,但是横向电梯的不论顺时针还是逆时针都能走到目的地,只是速度略有差异。因此横向电梯怎么走也成了让人很头疼的事情。如果根据顺逆时针行走到达目的地的快慢,将请求分为顺时针与逆时针的话,电梯在实际运行过程中为了接客而改变方向的策略实现起来实在是太过复杂,任何一种频繁更换方向的方法,几乎都能被一组特殊数据给击溃,所以我放弃了那些复杂的方法,选择了返璞归真。
我的横向队列是不分方向的,所有人来了都是在一个队列里等,而电梯则是看到人就往里加,除非加满了。而强测得性能分,也从事实角度证明过度考虑复杂的调度策略是没什么必要的。
3.横向电梯的调度策略
我用的调度策略是自己想的一个简单策略:
①初始时按顺时针运行
②Stop以后再次被唤醒判断一下左侧两楼座与右侧两楼座那一边人多,并将方向设置成此方向。然后一直按此方向运行。
三、第七次作业
1.基本思路
用MultiPersonRequest类继承了PersonRequest类,并采用一个类似"流水线"的生产模式,将请求拆分为若干个生产阶段,且将部分完成的请求重新投入调度器,继续生产步骤。此外,原先的线程终止条件不再适用,要添加Finisher类来负责线程的终止。
四、心得体会
本单元学习了多线程编程,这算是第一次接触这样的复杂的设计,通过对电梯模拟调度的设计,我的OO能力得到了较大的提高,对于面向对象的设计方法,也有了更加深刻的认识。