269
社区成员




本单元要求我们实现一个多个电梯的协作系统,主要目标是:
三次作业对我们的要求分别是:
第一次作业:每个乘客只可以被分配给固定的电梯,每个电梯对自己要处理的队列进行决策如何追求最高的效率
第二次作业:
Schedule
请求,相当于一次中断处理。Receive
约束,必须先Receive
再进入这一部分主要是考察的我们在程序中对synchronized
,wait
,notifyall
的使用:
我们要在合适的地方对一个对象加上锁,并合理的释放锁,防止它长时间占有。这里主要需要运用好synchronized
,对方法、对象等加锁。
当电梯没有请求需要处理的时候,要让他释放锁,并且进入wait
状态,直到其他线程对队列进行更改时,用notifyall
唤醒电梯并重新进行决策。
具体实现如下:
synchronized
的使用示例:
public class RequestList {
private final ArrayList<Person> requestQueue = new ArrayList<>();
private boolean isEnd = false;
private int remain = 0;
public synchronized void offer(Person request) {
int i;
for (i = 0; i < requestQueue.size(); i++) {
if (request.getPriority() > requestQueue.get(i).getPriority()) {
break;
}
}
requestQueue.add(i, request);
notifyAll();
}
public synchronized Person removeFirst() {
if (!isEmpty()) {
return requestQueue.remove(0);
} else {
return null;
}
}
......
}
在几个队列类中我都将它们中所有的方法都进行了synchronized
包裹。因为每一个队列都会在不同的现场中被读或写,而要保证每一次访问它时,不会造成冲突,那么就要让它的每一个方法都带锁。
不过其实可以让它的方法分为两类:读操作和写操作,合理利用读写锁可以更高效率的对这个对象进行访问,不过由于代码运行的时间相比于线程处理的时间可以忽略不计,使用没有实现读写锁,只用了简单的synchronized
。
wait
和notifyall
的协作:
public synchronized void waitRequest() {
if (isEmpty() && !isEnd()) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
我的代码中的wait
只会在电梯发现队列为空,但还不能终止的时候,调用waitRequest
时才会使用。可能一些其他同学会在读操作中使用,但我觉得它并没有必要等待,可以直接返回null
。
那已知会使用wait
的条件,我们就只需要在这些条件有可能被更改时再进行notifyall
即可。那么我只有在增加队列中请求个数offer
和标记输入结束setEnd
时才需要唤醒(在程序中可能还有一些特例),其余的唤醒只会让电梯重新获取策略,造成不必要的资源浪费。
在这里列举一些分派策略的实现思路:
1.随机分配,直接id = r.nextInt(6)+1
即可。
2.均匀分配,对6取余+1,跳过不能分配的。
3.评价体系,按照性能分指标,为当前电梯状态的每个参数赋值一个权重,得到一个综合分,每次都分给分数最高的那个。
4.影子电梯:每当新增了一个乘客请求,在真正分配之前我们分别模拟一下将其分给6个电梯哪个效果最好。那么我们就要给每个电梯当前状态建立一个“影子”,让影子去提前跑一个结果,只要用时间变量time
的增加取代,sleep
部分即可,这样就可以得到用时最短的那个方案。
我参考了现实中我们等电梯的等待方式。我们会一群人在几个电梯中间,不去只关注一个电梯,而是哪个电梯到了,我们尝试往里面“挤”。那么分派的标准就不是来一个人就要让他必须去等那一个电梯,而是每当一个电梯到达了一个楼层,在该楼层中选择人上电梯(也就是我们自己挤上去的过程)。那么我就完全省去了分派这一过程。
具体实现:和每一个电梯占有自己的子队列一样处理,只是每个电梯都共享了同一个子队列。
性能分析:在我的想法中,而分派过程是考虑了一个暂时最优的策略,将请求直接分走。而每一个电梯对自己的子队列都有一个调度策略,这个策略是与时俱进的,可以不断更新队列。那么省去分派过程就是只采用电梯调度策略,一定性能优于先分派再调度。
不过这个方法需要经常对队列加锁,更有可能出错,读写操作也需要更多时间。不过我认为这个时间并不会大于性能带来的好处。
主要是在Look
算法的基础上,增加了优先级的策略ALS
:
Look
已经保证方向,并没有影响)策略类实现如下:
if (schedule != null) {
return Advice.SCHEDULE;
}
if (receivePerson != null) {
if (curFloor != receivePerson.getFromFloor()) {
return Advice.MOVE;
} else {
return Advice.OPEN;
}
}
if (needOpenForOut(curFloor, destMap) ||
canOpenForIn(curFloor, curNum, direction, requests)) {
return Advice.OPEN;
}
//如果电梯里有人
if (curNum != 0) {
return Advice.MOVE;
}
//如果电梯里没有人且无法进人
else {
//如果请求队列中没有人
if (requests.isEmpty()) {
if (requests.isEnd() && scheRequests.isEnd()) {
return Advice.OVER; //如果输入结束,电梯线程结束
} else {
return Advice.WAIT; //如果输入未结束,电梯线程等待
}
}
//如果请求队列有人
return Advice.RECEIVE;
}
SCHE
请求优先级最高。
由于我的所有电梯共享一个队列,所以不存在receive到的子队列,那么我每次只在电梯每人时找一个优先级最高的receivePerson
,先把他接到再进行其他操作。所以RECEIVE
优先级很高。
每当电梯到达一个楼层需要确定是否开门。即可能有人到了,也有可能等待队列中该楼层有人要往相同方向走。(当没人时可以找不同方向的)
如果不用开门,且电梯有人,就正常运行到direction
上的下一个楼层。
如果没人且不需要开门,那么如果队列为空,就决定继续等待还是终止线程。如果不为空,则去找一个receivePerson
接他。
输入线程InputThread
将请求输入给RequestList
,再通过DispatchThread
分派给每个电梯的子队列RequestTable
。电梯线程中封装一个Strategy
策略类,实现电梯类和策略类分离,每次将电梯的状态和RequestTable
传入进行决策返回建议Advice
。电梯线程中通过得到的Advice
来运行。
相比于上一次直接删去了子队列类。其余就是增加了ScheRequestLlist
的输入和分派,以及在电梯线程中增加了schedule
的完整方法。其余分派和receive实现见上文。
我将UPDATE
请求同样放入ScheRequestList
中进行分派,省去了一定的代码量。新增了UpdateHandler
用于储存UPTADE
之后的信息。我没有新开双轿厢电梯类,而是把电梯和UpdateHandler
同时传入新的策略类UpdateStrategy
,即可得到双轿厢电梯的信息和另一个电梯的状态。UPDATE
后就舍弃Strategy
,只用UpdateStrategy
进行决策,这样可以保证原来的正常电梯运行不受影响,Strategy
也不用更改。
稳定的内容:
SCHE
和UPDATE
这类特殊请求的分派和处理。易变的内容:
电梯线程的不同操作
队列的读写操作
新情况下策略类Strategy
的决策方式
//电梯线程中识别到要进行改造
synchronized (updateHandler) {
updateHandler.increaseReady();
if (!updateHandler.isReady()) {
try {
updateHandler.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
//UpdateHandler工具类,让两者同一储存
public synchronized void increaseReady() {
ready++;
if (isReady()) {
TimableOutput.println("UPDATE-BEGIN-" + getElevatorAId()
+ "-" + getElevatorBId());
}
notifyAll();
}
其他如让电梯里乘客下电梯,等待一秒省略。
当结束时,也与上述相似,等到两个都完成时,统一输出。
每一步要进行移动之前都判断是否移动会碰撞,如果前方有电梯占用,则返回WAITTOMOVE
,即等待一段时间(sleep
)再来重新查询:
if (curNum != 0) {
if ((direction && curFloor == transferFloor - 1 &&
anotherElevator.getCurFloor() == transferFloor) ||
(!direction && curFloor == transferFloor + 1 &&
anotherElevator.getCurFloor() == transferFloor)) {
return Advice.WAITTOMOVE;
}
return Advice.MOVE;
}
即便是返回的是MOVE
也要特判一下,因为有可能两个电梯都往transferFloor
移动,还没移动到,另一个就得到了advice
。因此,需要先等待0.2秒,然后再判断此时另一个电梯是否在transferFloor
,如果不是才可以进行真正的移动。
前两次作业都没有出现bug
第三次作业由于写的比较匆忙,留下了一些隐患:
在update要把人赶出去的时候,clear
写错了位置。
同步开始改造的begin
输出有问题,没有完全同步
update后wait的实现有问题,导致轮询
debug方法
通过评测机找到错误位置后,根据输出看为什么会发生这种错误,可能在什么地方的判断有问题
我的主要问题主要还是轮询,所以我会在每个可能导致轮询的地方加上输出,看什么时候会突然输出很多行,然后看是什么地方的wait或加锁错误。
多线程编程中最容易出的问题就是线程安全问题。我个人采用的主要是synchornized
对于共享对象的方法加锁与部分引用处synchronized(obj)
加锁。在刚开始,我主要是根据实验课的代码进行修改,没有完全理解自己写的代码,后来在改代码的过程中才慢慢理解。通过本单元学习,我认为应该注意以下的线程安全问题:
wait
判断什么时候应该唤醒它,在其他方法处是否加了正确的notifyAll
,以及在while
循环中使用wait
等待,不要滥用notifyall
,避免意外唤醒ConcurrentModificationException
问题wait
。InputHandler
输入,DispatchThread
调度派发,ElevatorThread
完成消费者功能与输出。同时为每个电梯封装实现自己的策略类,不要全都一股脑在电梯线程中实现。