BUAA OO-Course 2022 Unit2 Summary
作者:互联网
第一次作业(hw5)
任务说明
本次作业的任务为对A、B、C、D、E五座大楼中的电梯进行实时调度。每一座楼中有一个电梯,可以在1-10楼之间运行,需要耗费时间的操作为上楼、下楼、开门、关门(乘客进出不需要时间,但需要在开关门之间完成)。
代码架构模式与调度策略
UML类图
UML协作图
代码架构模式(含同步块设计)
|- Main: 主类
|- InputThread: 输入类,一个线程对象
|- DistributeThread: 分配器,一个线程对象
|- ElevatorThread: 电梯线程,共五个线程对象
|- ElevatorState: 电梯运行状态
|- Output: 输出类
|- PersonRequestQueue: 共享对象,将多个PersonRequest对象封装成一个队列。程序中该类有三种不同的对象
|- PersonRequest: 官方输入包提供,记录每个请求
程序的架构主要参考了上机实验课提供的策略。首先在main
方法中启动一个InputThread
对象,不断读入信息,接着启动五个电梯线程ElevatorThread
对象和一个分配器DistributeThread
对象。其中,有两个重要的共享对象实例:PersonRequestQueue waitqueue
和ArrayList<PersonRequestQueue> outsides
。输入线程和分配器共享waitqueue
对象,而五个电梯线程分别与分配器共享outside(outsides中的元素)
对象。输入线程不断从读取输入信息,将得到的用户电梯请求存入waitqueue
并通知分配器,而分配器则在得到通知后从waitqueue
中读取请求,并根据请求所在的大楼编号将该请求存入outsides
中对应的outside
中,然后通知电梯线程,电梯线程得到通知后则进行调度处理。
可以看出,代码在进行对不同大楼电梯的请求进行分配时采用的是生产者-消费者模式,两对生产者-消费者关系如下:
- 第一对:
- 生产者:输入线程
InputThread
- 消费者:分配器
DistributeThread
- 托盘(共享对象):
PersonRequestQueue
类型的waitqueue
对象
- 生产者:输入线程
- 第二对:
- 生产者:分配器
DistributeThread
- 消费者:电梯线程
ElevatorThread
- 托盘(共享对象):
PersonRequestQueue
类型的outside
对象
- 生产者:分配器
由于不同线程是通过共享对象进行通信的,因此notifyAll()
和wait()
方法都需要在PersonRequestQueue
类的共享对象实例的方法调用,而不应该在电梯线程或分配器中使用。例如,当分配器发现waitqueue
等待队列中没有新的请求时,应在它调用的PersonRequestQueue.getOneRequest()
中使用wait()
;并且,如果其被唤醒后对共享对象进行了修改,则应在退出方法前调用notifyAll()
,通知其他引用这个共享对象并可能在wait
状态的线程:(这也是本次作业同步块设计的一个要点,即调用wait()
方法的monitor应该和同步对象相同,故我在该共享对象中调用wait()
)
/* in class PersonRequestQueue */
public synchronized PersonRequest getOneRequest(boolean remove) {
if (/* 没有需求且未终止 */) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// do something
notifyAll();
return personRequest;
}
这样才能保证不同线程之间的正常交互,既不浪费CPU资源,同时也不浪费时间。
调度策略
本次作业基本上采用的是look策略(跟现实情况基本相同),即当电梯里没人时,外面有请求则前往请求所在楼层,否则挂起;当电梯里有人的时候,则按照最开始进入电梯的人的方向运行,中途经过的楼层如果有人在外面等并且他走的方向与电梯当前方向相同,则让他进来,否则不让他进来。而在电梯最后一个人出去后,如果当前前方有请求则沿着原方向进行;如果当前方向前方没有请求但是反方向有请求则立刻改变方向(不用到楼顶或楼底);否则如果什么请求都没有则挂起。
我在设计策略时,直接将其写在了ElevatorThread
的run()
方法中。这里具体分析一下:
public void run() {
while (true) {
if (/* 符合终止条件 */) {
return;
}
if (innerCanGoOut()) {
openAndClose();
/* 包含让电梯里的人出去和让外面的人进来两个操作。
在让电梯里的人出去后,需要重新判断outerCanGetIn() */
} else if (outerCanGetIn()) {
// outerCanGetIn()包含判断是否已经满载
openAndClose();
} else if (!isInnerEmpty() || hasRequestAhead()) {
moveAlongOriginalDirection(); // 沿着原方向前进
} else if (!isOutsideEmpty()) {
changeDirection();
} else {
// ...
elevatorState = ElevatorState.Hung; // 挂起
outside.outsideWait(); // 在PersonRequestQueue.outsideWait()调用wait()
}
}
}
可以看到调度代码采用了几个if-else
的的罗列,这里的条件是有先后顺序的。
-
第三行的条件判断是否线程终止条件。
-
第六行判断电梯内的乘客是否能出去,如果能则进行开关门操作。
-
第十行则是在电梯内的人全部不能出去的情况下判断外面的人是否能进来,如果能的话则进行开关门操作。这里的
outerCanGetIn()
不仅需要该请求的起始位置fromFloor
与电梯所在楼层相同,还需要其目的楼层toFloor
在运行方向上。 -
注意,由于第六行只判断了电梯内的人可以出去而不知道外面的人是否可以进来(或者一开始满载,
outerCanGetIn()
返回false,但是里面的人出去后外面的人就能进来了),因此,在所调用的openAndClose
方法中,里面的人出去后需要重新调用outerCanGetIn()
判断来决定是否执行outerGetIn
方法,以保证电梯尽可能多的载人。 -
当第三行与第六行条件都不满足时,说明本楼层电梯不需要开关门。于是第十行判断当前里面是否有人,或者当前电梯运行方向上是否有人在等待
hasRequestAhead()
(该等待的人只要fromFloor
在电梯运行前方就行,其前往的方向不确定,即有可能他前往的方向与当前电梯运行方向相反),如果这两个条件有一个满足,都说明电梯需要继续沿着原方向前进。 -
如果上述条件都不满足,即当前楼层没有上下客请求、电梯内没人且电梯运行方向的前方没人等待,则此时电梯判断是否整栋楼都没有等待的人,如果有就改变方向,如果没有则挂起。
-
最后补充一个上述看似冲突的两个方法:
outerCanGetIn()
和hasRequestAhead()
。前者需要乘客前往的方向在当前电梯运行方向上,而后者则只需乘客的起始楼层在当前电梯运行的方向上。然而实际上并没有冲突,用下面这个例子说明:当前电梯在1楼,前方有个起始楼层为6楼,目的楼层为4楼的请求。则程序一开始调用
hasRequestAhead()
发现条件满足,移动到了6楼,且当前电梯的运行方向为向上。在6楼判断outerCanGetIn()
时发现乘客方向和电梯当前方向不一样,于是返回false;而在判断hasRequestAhead()
时同样返回false。于是当判断!isOutsideEmpty()
时,发现可以,则进行转向。于是在下一次循环中,该乘客与电梯的方向均为向下,乘客便能顺利地上电梯了。
复杂度分析
第一次作业复杂度分析表格[1](为了观感将其放在最后,可点击链接进行查看和返回)
本次作业除了ElevatorThread
以外的类复杂度都较低,而ElevatorThread
的复杂度主要集中在调度过程的判断上。run
方法的条件判断分支较多导致复杂度较高,而hasRequestAhead()
则由于先对边界条件(电梯是否到顶楼或底楼、当前电梯是否挂起、外面请求是否为空)进行了一个判断后再开始用循环迭代分析。由于这些边界条件基本上都是必要的,可以有效防止电梯超出楼层或者跑到地下等情况,所以暂时找不到降低其复杂度的办法。
总体上来说,复杂度还是可以接受的。
BUG分析
-
本次作业在中测、强测和互测中均没有致命bug,其中有一个小问题导致强测差点超时。在上面提到的
openAndClose()
方法中,有如下代码:if(innerCanGoOut){ innerGoOut(); } if(outerCanGetIn){ outerGetIn(); }
而在电梯线程的
run
方法中,先执行innerCanGoOut()
,并在该方法中设置innerCanGoOut
变量的值,如果为true,就直接调用方法openAndClose
,因此忘记对outerCanGetIn
进行变量更新。这导致只要当前楼层有人出电梯,外面的人就进不来,电梯需要多进行一次“开门”和“关门”操作。在发现问题后,我在innerGoOut
的最后增加了对outerCanGetIn()
的调用,只要电梯有人出,就更新这个变量。 -
互测中有遇到没有对输出进行同步处理的错误:
Output not in order
-
互测中还hack到了下面要讲的并发修改异常。
一些收获
其实在上次作业就提到了并发修改异常ConcurrentModificationException
并不一定每次都会产生。这里就多线程与其的关系做一些补充。
-
在单线程情况下,如果是用迭代器遍历,并在遍历的过程中要删除迭代序列中的某一项的话,应用iterator的remove方法,而不是直接在列表上进行删除。
Vector<Item> list; // .... Iterator it = list.iterator(); while (it.hasNext()) { Item item = it.next(); if (/*...*/) { //.... it.remove(); // 不会报并发修改异常 // list.remove(item); 会报并发修改异常 } }
-
在多线程的情况下,上述方法就不推荐使用了。尽管例子中使用的是线程安全的容器
vector
,但是其产生的迭代器并不是同步的,此时应该用常规的循环进行迭代,并且在共享对象内对其进行删除,而不是在当前对象中直接从列表删除。Vector<Item> list; // ..... ItemList itemList = new ItemList(list); // 将list封装 for (int i = 0; i < itemList.length(); i++) { Item item = itemList.get(i); //..... itemList.removeItem(item); } /* ItemList 封装类部分定义如下 */ public class ItemList { private Vector<Item> list; public ItemList(Vector<item> list) { this.list = list; } /* synchronized 的使用保证线程安全 */ public synchronized Item get(int i) { return list.get(i); } public synchronized void removeItem(Item item) { list.remove(item); } }
第二次作业(hw6)
任务说明
本次作业在上次作业A、B、C、D、E五座楼分别各一台电梯的情况下有所增加。
第一个变化:原先每座楼电梯的数量由固定的1个转化为不固定的多个——初始时各座默认一台,随后根据输入数据ADD-building-电梯ID-楼座ID
动态增加电梯数量。
第二个变化:在不同楼座的相同楼层间增加了横向、环形电梯,该电梯可以沿着A→B→C→D→E→A
(在本文定义为向右)或A→E→D→C→B→A
(在本文定义为向左)方向运行。
在这次作业中规定了输入的请求要么在同一楼座内,要么在不同楼座的同一楼层内(即不需要换乘电梯)。
代码架构模式与调度策略
UML类图
UML协作图
代码架构模式
|- Main: 主类
|- InputThread: 输入类,一个线程对象
|- DistributeThread: 分配器,一个线程对象
|- VerticalElevator: 纵向电梯线程,初始时共五个线程对象,之后可在InputThread中动态增加
|- HorizontalElevator: 横向电梯线程,初始时不创建,之后可在InputThread中动态增加
|- ElevatorState: 电梯运行状态
|- Building: 当前(横向)电梯所处楼座,并有移动楼座、判断最短距离以及方向的方法
|- Output: 输出类
|- PersonRequestQueue: 共享对象,将多个PersonRequest对象封装成一个队列。
# 下面两个没有放在UML图中,由官方包提供:
|- PersonRequest: 官方输入包提供,记录每个乘客请求
|- ElevatorRequest: 官方输入包提供,记录增加电梯的请求
本次作业在上次作业的基础上进行,同样继续使用生产者-消费者模式。由于我粗暴地位横向电梯单独开了一个类,生产者-消费者的对应关系就从两对变成了下面三对:
-
第一对:
- 生产者:输入线程
InputThread
- 消费者:分配器
DistributeThread
- 托盘(共享对象):
PersonRequestQueue
类型的waitqueue
对象
- 生产者:输入线程
-
第二对:
- 生产者:分配器
DistributeThread
- 消费者:纵向电梯线程
VerticalElevator
- 托盘(共享对象):
PersonRequestQueue
类型的outside
对象
- 生产者:分配器
-
第三对:
- 生产者:分配器
DistributeThread
- 消费者:横向电梯线程
HorizontalElevator
- 托盘(共享对象):
PersonRequestQueue
类型的outside
对象
- 生产者:分配器
关于生产者与消费者线程之间的通信以及同步处理基本与第一次作业相同,这里不再赘述。
从UML类图中可以看到新增了一个Building
类,这个类内部只有一个char
类型的属性,用于存放楼座对应的字符。由于横向电梯从A到E五个楼座可以循环移动,在Building
类中封装了向左、向右等移动方法,增强了代码可读性。比如,电梯向左移动一个楼座的行为可以封装如下:
public void goLeft() { // A → E → D → C → B → A
if (building == 'A') {
building = 'E';
} else {
building -= 1;
}
}
当然,这个封装非常容易实现。
调度策略(含同步块设计)
调度策略是本次作业的重点,主要为:
- 单部横向电梯的调度(与单部纵向的不同?循环?)
- 多部同一楼座的纵向电梯或同一楼层的横向电梯的调度
首先说明本次作业中单部横向电梯调度的思路。
与单部纵向电梯调度类似,横向电梯借鉴了纵向电梯的代码,如下所示:
// 纵向:具体说明见第一次作业的调度策略部分
public void run() {
while (true) {
if (/* 符合终止条件 */) {
return;
}
if (innerCanGoOut()) {
openAndClose();
} else if (outerCanGetIn()) {
openAndClose();
} else if (!isInnerEmpty() || hasRequestAhead()) {
moveAlongOriginalDirection();
} else if (!isOutsideEmpty()) {
changeDirection();
} else {
/* 电梯挂起,等待 */
}
}
}
// 横向
public void run() {
while (true) {
if (/* 符合终止条件 */) {
return;
}
if (innerCanGoOut()) {
openAndClose();
} else if (outerCanGetIn()) {
openAndClose();
} else if (!isInnerEmpty() || hasRequestAhead()) {
moveAlongOriginalDirection();
} else if (!isOutsideEmpty()) {
setDirectionByOutsiders(); // difference here!!!
} else {
/* 电梯挂起,等待 */
}
}
}
从顶层的架构(run方法内)看,两种电梯调度基本相同,但在一些细节上不同之处还是很明显的。首先,对于一部横向电梯,在到达一个楼层后,先判断里面的人是否可以出去,再判断外面的人是否可以进来,如果上面两个条件均不满足,则判断电梯里是否还有人或者电梯前方是否还有需求,如果满足则沿着电梯运行方向继续移动一个楼座,如果仍不满足,则判断电梯外是否还有需求,有的话则根据等待者重新设置电梯方向。
可以看出,横向电梯的调度中没有了“反转方向”的行为。在横向电梯调度中,不同之处即为上面加粗的两个点,分别对应hasRequestAhead()
和setDirectionByOutsiders()
两个方法。
对于判断电梯前方是否还有需求的hasRequestAhead()
方法而言,首先需要定义什么叫做RequestAhead
。由于电梯是环形的,电梯一直沿着一个方向都可以接到请求,但这显然会导致时间巨额开销。我这里定义的ahead
是指前方请求所在楼座距离当前电梯所在楼座不超过2(A到D和C的距离均为2,方向分别为向左、向右)。比如,当前电梯在A座,运行方向向右,而外面有个起始楼座在E的请求。尽管电梯只要向左移动一座就能接到,但是由于其不在电梯运行方向上,该E座的请求不能算作Request Ahead
。
而对于根据等待者重新设置电梯方向的setDirectionByOutsiders()
方法来说,由于该方法的执行条件是在电梯内已经没有请求的前提上,因此只需要根据外面的请求来决定方向。在该方法中,通过调用Building
类的静态方法Building.getMinDistance()
来判断乘客所在楼座距离当前电梯楼座的距离,进而判断电梯应当向哪边移动。
第二部分的调度问题为多部同一楼座的纵向电梯或同一楼层的横向电梯的调度。
与上一次作业相比,该作业有一个重大的不同。对于同一楼座的多部电梯,由于我采用的是自由竞争的策略,即共享同一个等待队列,在读取请求时容易发生数据冲突。为此,我在对一部电梯判断当前楼层(或楼座)外面的人能否进电梯时,如果能进,直接将他们从共享的等待队列中移除,并加入到本电梯私有的候乘队列,该方法自然也必须进行同步,如下所示:(如下也是本次作业同步块设计的重点,但是如下设计会导致性能上的些许下降)
/* 以纵向电梯为例,该方法在指定楼层尽可能的读取请求,从共享队列中删去后并返回 */
// PersonRequestQueue.getAllFromFloorAt()
public synchronized ArrayList<PersonRequest> getAllFromFloorAt(
int floor, int maxNum, ElevatorState direction) {
// floor:起始楼层为floor的需求
// maxNum:最多返回的人数
ArrayList<PersonRequest> outsiders = new ArrayList<>();
int inNum = 0;
for (int i = 0; i < personRequests.size(); i++) {
PersonRequest req = personRequests.get(i);
ElevatorState outsiderDirection =
(req.getToFloor() > req.getFromFloor()) ?
ElevatorState.Up : ElevatorState.Down;
if (req.getFromFloor() == floor && (outsiderDirection == direction)) {
outsiders.add(req);
personRequests.remove(i);
i--;
inNum++;
if (inNum >= maxNum) {
break;
}
}
}
return outsiders;
}
该同步方法设在共享对象所处的类中,传入电梯所在楼层floor
,电梯最大可进人数maxNum
以及当前电梯的状态(方向)direction
,从多部电梯共享的等待队列personRequests
中读取尽可能多的请求,删去这些请求后将它们加入outsiders
并返回。这就保证了多部电梯不会同时读到相同的请求,避免了"一个人同时进两步电梯"的错误。
当然,由于该同步方法较复杂,电梯的性能会有所下降。
复杂度分析
第二次作业复杂度分析表格[1:1](为了观感将其放在最后,可点击链接进行查看和返回)
与第一次作业相比较,第二次作业的复杂度有所增加。在类复杂度上仍然主要是集中在电梯类,而方法复杂度上,由于增加了多部电梯共享同一等待队列的自由竞争策略,在判断请求相关的方法上需要进行同步,因此,这些方法的复杂度有所提升。
BUG分析
-
课下自己测试时出现了如下java标准异常:
Exception in thread "..." java.lang.IndexOutOfBoundsException: Index: 0, Size: 0 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.get(ArrayList.java:433) at HorizontalElevator.setDirectionByOutsiders(HorizontalElevator.java:202) at HorizontalElevator.run(HorizontalElevator.java:41)
从报错信息可以看出,是在空数组中访问了下标0所在的元素,出错代码如下:
// HorizontalElevator.outerCanGetIn() // ..... ArrayList<PersonRequest> outsideRequests = outside.getAll(); for (int i = 0; i < outsideRequests.size(); i++) { PersonRequest outsider = outsideRequests.get(i); // .....
而
getAll()
方法为共享对象中的方法:public ArrayList<PersonRequest> getAll() { /* 注:该方法不同步!!!! */ return personRequests; }
可以看到,该方法在将共享对象返回出去后,为了速度,我并没有在
HorizontalElevator.outerCanGetIn()
对该对象进行synchronized
同步处理,因此多部电梯同时取对象就可能导致如上异常。为此,我采用了粗暴地解决方法:// HorizontalElevator.outerCanGetIn() // ..... ArrayList<PersonRequest> outsideRequests = outside.getAll(); for (int i = 0; i < outsideRequests.size(); i++) { PersonRequest outsider; try { outsider = outsideRequests.get(i); } catch (IndexOutOfBoundsException e) { return false; } if (outsider == null) { return false; } // ......
-
互测时被人Hack出了轮询(
CPU TIME LIMIT EXCEEDED
),原因在于我在共享对象的isEnd()
方法中加了没有必要的notifyAll()
public synchronized boolean isEnd() { notifyAll(); // here return isEnd; }
第三次作业分析(hw7)
任务说明
本次作业主要有以下两个不同点:
- 扩大乘客移动需求:即乘客的出发楼座和出发楼层、目的楼座和目的楼层均可以不同,这就使得乘客一定需要换乘;
- 电梯定制化:可以定制电梯的运行速度、可容纳人数、是否能在某座开关门(横向电梯可设置)
具体来说,初始时在五座分别有一个纵向电梯,同时还有一个在1层的可达所有楼座的横向电梯。在程序运行中间可以增加定制化的电梯。
代码架构模式与调度策略
UML类图
UML协作图
下图省略了构造方法、getter/setter方法
代码架构模式(含同步块设计)
package io: 输入输出相关
|- Main: 主类,负责处理输入,以及当输入结束时设置完成标志
|- Ouput: 输出类
package control: 控制相关的类
|- Controller: 负责分配请求,对需要换乘的请求拆分成多步请求
|- Counter: 计数器,负责在输入完成后判断是否所有请求都完成
|- HorizontalMap: 负责记录横向电梯的可达楼座信息
package elevator: 电梯相关类
|- Building: 作为电梯类的一个属性,记录电梯当前所处楼座和可达楼座信息
|- ElevatorState: 枚举类,代表电梯"上升、下降、挂起、向左、向右"五个状态
|- HorizontalElevator: 横向电梯类
|- VerticalElevator: 纵向电梯类
package requestes: 请求相关类
|- HorizontalReqQueue: 横向电梯等待队列类
|- VerticalReqQueue: 纵向电梯等待队列类
|- Req: 请求类,对官方包PersonRequest的封装
对本次作业,我进行了重构。下面先对各个类进行进一步说明:
采用单例模式的类有:
Ouput,Controller,Counter,HorizontalMap
作为线程的类有:
Main(主线程),HorizontalElevator,VerticalElevator
由于本次作业涉及java类较多,我将类分到了四个包中,以方便对他们进行管理。与前两次作业结构上相似的类为两个电梯类以及Building
楼座信息记录的类;而前两次作业中的输入线程类Input
、分配线程类DistributeThread
则被删去,由主类Main
处理输入,由单例模式的控制器Controller
将请求分配i.e.调用addRequest()
到合适的队列。
另外,上一次作业中的共享队列的类为PersonRequestQueue
,其内部简单地用ArrayList
实现,该类作为纵向电梯、横向电梯同一的共享队列;而本次作业则将共享队列类进行细分,分为横向电梯请求队列HorizontalReqQueue
和纵向电梯请求队列VerticalReqQueue
,其内部改用HashMap<>
实现,加快了查找速度,具体如下:
/* in class HorizontalReqQueue */
// 横向电梯的等待队列
// 即:请求所在楼座为键,请求为值
HashMap<Character, ArrayList<Req>> horizontalReqs;
/* in class VerticalReqQueue */
// 垂直电梯的等待队列
// 即:请求所在楼层为键,请求为值
HashMap<Integer, ArrayList<Req>> verticalReqs;
由于本次作业出现了一定需要换乘电梯的情况,我对乘客请求PersonRequest
类进行了封装,在Req
类中,记录下面几个信息:当前乘客所处楼座和楼层、乘客最终目的楼座和楼层、乘客当前子请求(即每一步请求)的目的楼座和楼层。其中,最后两个量需要由控制器设置(具体见下面调度策略分析)。
此外,在程序运行过程中会出现一个问题,由于每个请求都有可能需要换乘,当输入停止并设置相关标志后,可能导致每个请求后续步骤需要使用到的电梯停止,造成某些请求无法完成。对此,我采用了研讨课上大家提出的方法,设置一个计数器,投入一个请求就加一,一个请求完成就减一,在输入停止后判断其值是否为0,如果不为零,就不能让电梯停止。计数器实现如下:(这是本次作业新增的一个同步块设计)
public class Counter {
// ......
private int count;
// ......
public synchronized void acquire() {
count++;
}
public synchronized void release(int num) {
count -= num;
if (count == 0 && inputEnd) {
notifyAll();
}
}
public synchronized boolean allReqFinished() {
// 仅在主线程调用(输入完成后启动)
inputEnd = true;
while (count != 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return true;
}
}
主线程在输入结束后调用单例模式类Counter
的allReqFinished()
方法并阻塞,直到最后一个请求完成后将其唤醒,返回true后在主线程设置结束标志。
那么,还剩下的一个类HorizontalMap
具体的功能与实现又是怎样的呢?
前面提到,该类主要记录横向电梯的可达楼座,每个横向电梯在其构造方法内,调用单例模式类HorizontalMap
相应增加电梯的方法:
HorizontalMap.getInstance().addElevator(floor, msg);
这样方便记录所有横向电梯的可达情况,方便在Controller
类在调度时直接调用。
由于所有电梯线程并发的访问该对象的方法并改变其数据,HorizontalMap
类同样也需要进行同步,其同步块设计如下:
public class HorizontalMap {
// ......
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public boolean canReach(int floor, char from, char to) {
lock.readLock().lock();
// 在floor层,从from座到to座是否有**直达**电梯
// ......省略多个return语句,在return前也需要释放读锁
lock.readLock().unlock();
return false;
}
/* 上面提到的在每个横向电梯的构造方法内调用的方法 */
public void addElevator(int floor, int switchInfo) {
lock.writeLock().lock();
// ......
lock.writeLock().unlock();
}
}
这里没有简单地用synchronized
方法,而是采用了课上所介绍的可重入读写锁,使得在调用canReach()
只读方法时可以有多个线程并发调用,提高了代码可读性和效率。
调度策略
就横向电梯而言,相同楼层的电梯仍然共享同一个等待队列,即仍然采用自由竞争策略(而我由于处置不当导致了轮询,具体见bug分析)。
由于时间等各种原因,本次作业的调度采用了相对简单地思路进行,具体的策略描述如下(仅列出横向调度):
-
对于一个起始楼座与目的楼座不同、起始楼层也与目的楼层不同的横向请求,首先判断两者楼层间是否存在能到达起始楼座和目的楼座的横向电梯:
-
若能,则以此设置该请求的当前子请求目的楼座和楼层;
-
如果不能,则按照距离从近到远在所有楼层中寻找是否有电梯同时可达起始楼座和目的楼座,直至找到一楼。
-
-
对于一个起始楼层和目的楼层相同、楼座不同的横向请求,如果该层存在某个横向电梯同时可达这两个楼座,则选择该电梯,否则按照距离从近到远在所有楼层中寻找横向电梯,直至找到一定存在可达横向电梯的一层。
该策略十分简单粗暴,具有如下不足:
- 对于同楼层不同楼座的请求,不能换乘。例如某请求从A-6到D-6,而该层有两部电梯,可达楼座分别为AB、BD,按照上述策略会判定为该层不可达,进而寻找其他楼层;
- 对于不同运行速度的电梯,调度时并不加一区分,一事同仁;
- 若有多个电梯,并没做人数上的平衡,可能导致某些电梯没有请求课载,而某些电梯满载;
- 在设定好子请求后,不能根据输入过程电梯状态变化(比如增加了某个更近的电梯)而改变请求,即为静态调度策略。
复杂度分析
第三次作业复杂度分析表格[1:2](为了观感将其放在最后,可点击链接进行查看和返回)
在类复杂度上,复杂度大的类还是两个电梯,与上次作业差不多。而方法圈复杂度上,除了电梯获取请求的相关方法复杂度较高,控制器中规划请求路线和调度的复杂度也较高。总体上来说还可以接受。
BUG分析
-
先说说自己的bug。在强测和互测互测中有
CTLE
了。前面也说到了,在自由竞争策略下,对不同电梯的唤醒机制没有处理好。用例子来说明,如果6层某电梯可达AB两座并处于wait
阻塞状态,当控制器将一个从6层C座到D座的请求放到该等待队列时,通过notifyAll()
机制会唤醒该电梯,但是该电梯在被唤醒后判断请求时又发现没有请求可接,于是又被阻塞起来。如此这般便导致了轮询。 -
再说说互测中发现的别人的一些问题:
-
在代码某个地方用了递归后报的异常:
Exception in thread "Thread-9" java.lang.StackOverflowError at FloorElevator.destination(FloorElevator.java:99) at FloorElevator.destination(FloorElevator.java:145) at FloorElevator.destination(FloorElevator.java:145) at FloorElevator.destination(FloorElevator.java:145) at FloorElevator.destination(FloorElevator.java:145) ...... // 以下省略多行
-
空指针引用:
Exception in thread "Thread-1" java.lang.NullPointerException at Requestqueue.getRequestscopy(Requestqueue.java:60) at Elevator.down(Elevator.java:245) at Elevator.down(Elevator.java:247) at Elevator.run(Elevator.java:153)
-
总结
本次多线程单元还算满意的结束了,但在知识点方面还是有许多遗憾。
在设计模式方面,只掌握了几个最基本的设计模式:生产者-消费者模式、观察者、单例模式、流水线模式、主从模式,还有很多没有学。
在java多线程机制上,较为熟练地掌握了synchronized
同步机制下线程通过wait()
和notifyAll()
通信,初步学会了Lock
的使用。但没有学习线程池、信号量以及java提供的一系列线程安全容器的实现。
最后特别感谢这位同学提供的评测机,对我课下bug修复提供了极大帮助!!!
附录
hw5复杂度分析表格
-
类复杂度分析:
Class OCavg OCmax WMC PersonRequestQueue 1.33 4.0 12.0 Output 1.0 1.0 3.0 Main 2.0 2.0 2.0 InputThread 2.0 3.0 4.0 ElevatorThread 3.69 8.0 59.0 ElevatorState 0.0 DistributeThread 3.0 5.0 6.0 Total 86.0 Average 2.61 3.83 12.29 -
方法圈复杂度分析
method CogC ev(G) iv(G) v(G) ......(省略大部分复杂度较低的方法) ElevatorThread.run() 11.0 3.0 9.0 10.0 ElevatorThread.resetDirection() 10.0 1.0 2.0 6.0 ElevatorThread.outerGetIn() 12.0 1.0 6.0 9.0 ElevatorThread.outerCanGetIn() 8.0 4.0 5.0 8.0 ElevatorThread.openAndClose() 6.0 1.0 6.0 7.0 ElevatorThread.innerGoOut() 3.0 1.0 3.0 3.0 ElevatorThread.innerCanGoOut() 3.0 3.0 2.0 3.0 ElevatorThread.hasRequestAhead() 14.0 8.0 6.0 13.0 DistributeThread.run() 9.0 4.0 5.0 6.0 Total 103.0 56.0 86.0 111.0 Average 3.12 1.70 2.60 3.36
hw6复杂度分析表格
-
类复杂度分析:
Class OCavg OCmax WMC Building 1.88 4.0 17.0 DistributeThread 4.0 7.0 8.0 ElevatorState 0.0 HorizontalElevator 3.375 7.0 54.0 InputThread 4.5 6.0 9.0 Main 1.0 1.0 1.0 Output 1.0 1.0 3.0 PersonRequestQueue 2.0 5.0 20.0 VerticalElevator 4.0 9.0 64.0 Total 176.0 Average 2.98 5.0 19.55 -
方法圈复杂度分析
method CogC ev(G) iv(G) v(G) ......(省略大部分复杂度较低的方法) Building.getDirection() 3.0 4.0 1.0 4.0 DistributeThread.run() 15.0 4.0 7.0 8.0 HorizontalElevator.hasRequestAhead() 11.0 6.0 5.0 9.0 HorizontalElevator.innerCanGoOut() 4.0 4.0 2.0 4.0 HorizontalElevator.outerCanGetIn() 14.0 6.0 6.0 10.0 HorizontalElevator.run() 11.0 3.0 9.0 10.0 HorizontalElevator.setDirectionByOutsiders() 9.0 4.0 3.0 6.0 PersonRequestQueue.getAllFromBuildingAt() 7.0 4.0 4.0 5.0 PersonRequestQueue.getAllFromFloorAt() 9.0 4.0 4.0 6.0 VerticalElevator.hasRequestAhead() 18.0 9.0 6.0 15.0 VerticalElevator.innerCanGoOut() 4.0 4.0 2.0 4.0 VerticalElevator.outerCanGetIn() 20.0 6.0 6.0 15.0 VerticalElevator.run() 11.0 3.0 9.0 10.0 VerticalElevator.setDirectionWhenHung() 9.0 4.0 3.0 6.0 Total 231.0 118.0 158.0 220.0 Average 3.91 2.0 2.67 3.72
hw7复杂度分析表格
-
类复杂度分析:
Class OCavg OCmax WMC control.Controller 3.4 12.0 17.0 control.Counter 1.4 2.0 7.0 control.HorizontalMap 2.25 5.0 9.0 elevator.Building 2.0 4.0 28.0 elevator.ElevatorState 0.0 elevator.HorizontalElevator 3.46 7.0 52.0 elevator.VerticalElevator 3.625 8.0 58.0 io.Main 3.0 6.0 15.0 io.Output 1.0 1.0 3.0 requests.HorizontalReqQueue 2.77 8.0 25.0 requests.Req 1.38 4.0 18.0 requests.VerticalReqQueue 2.77 8.0 25.0 Total 257.0 Average 2.62 5.90 21.41 -
方法圈复杂度分析
method CogC ev(G) iv(G) v(G) ......(省略大部分复杂度较低的方法) control.Controller.setTransferWay() 23.0 12.0 12.0 13.0 control.HorizontalMap.canReach() 6.0 5.0 5.0 6.0 elevator.Building.getDirection() 3.0 4.0 1.0 4.0 elevator.HorizontalElevator.hasRequestAhead() 14.0 7.0 5.0 8.0 elevator.HorizontalElevator.outerCanGetIn() 3.0 4.0 1.0 4.0 elevator.HorizontalElevator.run() 11.0 3.0 9.0 10.0 elevator.VerticalElevator.changeDirection() 6.0 6.0 4.0 6.0 elevator.VerticalElevator.hasRequestAhead() 15.0 6.0 3.0 12.0 elevator.VerticalElevator.run() 11.0 3.0 9.0 10.0 elevator.VerticalElevator.setDirectionByInsiders() 8.0 5.0 3.0 6.0 requests.HorizontalReqQueue.getRequestAtBuilding() 12.0 7.0 4.0 9.0 requests.HorizontalReqQueue.hasRequestAtBuilding() 4.0 4.0 2.0 4.0 requests.HorizontalReqQueue.setDirectionByNearest() 7.0 5.0 3.0 6.0 requests.Req.needTransfer() 3.0 4.0 1.0 4.0 requests.VerticalReqQueue.getRequestAtFloor) 14.0 6.0 4.0 10.0 requests.VerticalReqQueue.setDirectionByNearest() 13.0 7.0 5.0 8.0 Total 277.0 185.0 204.0 292.0 Average 2.82 1.88 2.08 2.979591836734694
标签:OO,请求,复杂度,BUAA,Summary,楼座,电梯,线程,4.0 来源: https://www.cnblogs.com/NormalLLer/p/16207457.html