301
社区成员
发帖
与我相关
我的任务
分享

稳定的部分:输入线程和调度线程。
易变的部分:电梯线程。在加入reset机制后,电梯线程的下一步可能返回到waitQueue中;在第七次作业中,我在原来6个电梯线程的基础上额外增加了6个电梯线程。
在第五次作业中,我主要实现了input线程处理输入,实现了elevator线程。由于第五次作业中乘客已经被指定运载电梯,所以我并没有加入调度器,而是直接在输入线程input中将乘客分配到各自电梯id对应的请求池中。
请求池用一个类RequestQueue实现,类内部用一个Arraylist容器管理六部电梯的乘客请求,用addRequest方法实现每部电梯等待请求队列的扩充,用getPerson方法实现从等待队列中取出请求,取出的请求需要满足乘客所在楼层等于电梯此刻所在楼层和乘客前往楼层方向与电梯此刻运行方向相同两个条件。
public synchronized Person getPerson(int floor, boolean isUp) {
Iterator<Person> iterator = persons.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
if (person.getFromFloor() == floor && (person.getToFloor() - person.getFromFloor() > 0) == isUp) {
iterator.remove();
return person;
}
}
return null;
}
还用getRequests方法返回请求池中的容器,并且判断是否需要电梯线程等待。当等待队列为空时,电梯内的乘客为空且输入线程还未结束时则需要电梯线程等待,当输入线程已经结束但乘客和等待队列都为空时,则返回null以便电梯线程结束,否则直接返回等待队列,以便实现策略类。为了避免同步块的冲突,我对RequestQueue中的每一个方法都加了锁synchronized和notifyAll。
public synchronized ArrayList<Person> getRequests(ArrayList<Person> passengers)
throws InterruptedException {
while (this.isEmpty() && !this.isEnd && passengers.isEmpty()) {
notifyAll();
this.wait();
}
if (this.isEmpty() && this.isEnd && passengers.isEmpty()) {
notifyAll();
return null;
}
notifyAll();
return persons;
}
在电梯执行每一步动作之前,都会向策略类Advice寻求建议,并且用枚举类Action表示电梯每一步的动作,包括,移动MOVE,开门OPEN和调转方向CHANGE。我用了效率较高的look策略。
public Action getAction() {
if (ifIn()/*若电梯还未满员且有人需要进入*/ || ifOut()/*若乘客中有人目的地与电梯所在楼层相同*/) {
return Action.OPEN;
}
else if (/*若电梯内还有人*/) {
return Action.MOVE;
}
else {
if (ifMove()/*若等待队列中有人出发楼层位于电梯运行方向上*/) {
return Action.MOVE;
}
else {
return Action.CHANGE;
}
}
}
然后是电梯线程的实现。在Elevator类中,我用boolean类型变量isUp判断电梯运转的方向。为了避免轮询,我在开头就判断线程是否需要等待和退出,若都不需要则向策略类寻求动作建议,然后根据建议执行对应的动作。
在第五次作业的强测和互测中,均出现了ConcurrentModificationException的bug。原因是我没有对电梯执行getAction方法加锁,导致getAction方法中的等待队列requests可能会同时被input线程修改,导致出现并发修改异常错误。解决方法是对getAction方法加synchronized语句块。
由于这次作业没有指定乘客搭载的电梯,我新增了调度器类Schedule,将输入线程作为一级生产者,调度器作为一级消费者,中间用一个RequestQueue作为六部电梯所有的需求池。然后调度器作为二级生产者,六部电梯充当二级消费者。经过权衡之后,我认为电梯调度并没有什么万全之策,所以我选择了比较容易实现的随机调度。
然后是有关电梯重置的实现。为了避免reset输入后电梯迟迟不响应的情况,在input线程收到reset请求之后,就立马将reset传入到对应的电梯类里。在电梯线程每一次循环的过程中,在寻求动作建议之前就首先判断是否重置。若重置,则立马开门将乘客全部赶出,并且将该部电梯的乘客和等待队列中的人返回到总的需求池中。此外,在调度器随机分配时,要注意不要分配到正在reset的电梯的需求池中了。
public int method() {
while (true) {
int i = new Random().nextInt(6);
if (!processingList.get(i + 1).getIfReset()) {
return i;
}
}
}
对于reset的响应太慢。原因是刚开始我只是将reset当作一般的请求,先放入总的wait队列,再用schedule线程将其分配到对应电梯。意识到reset的高优先级后,我在input阶段就将其分配到各部电梯。
有可能将请求分配到正在reset的电梯。出现此bug的原因是我对reset信号量的处理不当,经过思考后,我发现这是一个读写问题,于是我利用了ReadWriteLock读写锁。读写锁允许多个线程同时读共享变量,但其写操作是互斥的。所以它的安全性和性能都较好。
public void setIfReset(boolean ifReset) {
rw.writeLock().lock();
try {
this.ifReset = ifReset;
} finally {
rw.writeLock().unlock();
}
}
public boolean getIfReset() {
boolean tmp;
rw.readLock().lock();
try {
tmp = this.ifReset;
} finally {
rw.readLock().unlock();
}
return tmp;
}
本次作业需要实现双轿厢电梯。在电梯类中,我加入了type变量判断电梯类型,transfer变量储存电梯的换乘楼层,此外,还用一个downfloor和upfloor储存每部电梯能够到达的最低层和最高层。为了结构和功能的解耦,我在初始时就创建了12个电梯和对应的12个请求池,如此,若input收到了DoubleCarReset请求,则将对应的电梯先重置为类型为A的下层电梯,再开启类型为B的电梯线程。
如何实现换乘:
为了不修改look策略,我采取解决人而不是解决事的方法。在随机分配给各部电梯后,先判断该部电梯是否双轿厢电梯和该人是否要换乘。若要换乘,则先暂时将其目的地改为换成楼层。到达换乘层后再将其目的地改为原始的目的地并返回到总的等待池中。
换乘会引发另一个问题,就是乘客从换乘楼层走出后想要返回等待池并进一步分配到不同电梯的等待池中,但这时候有可能每个电梯的等待池已经被setEnd且等待池和乘客为空了,这时电梯线程就会结束。为了避免这种情况,我设置了一个全局变量flag用来判断是否全体乘客都已到达目的地。在输入一个乘客时addFlag,在一个乘客到达目的地时subFlag。只有flag为0时电梯的等待池才能被setEnd。
if (wait.isEmpty() && wait.isEnd() && flag == 0) {
for (int i = 1; i <= 6; i++) {
ArrayList<RequestQuene> requestQuenes = processingList.get(i);
requestQuenes.get(0).setEnd();
requestQuenes.get(1).setEnd();
}
break;
}
如何防止双轿厢电梯在换乘楼层撞车:
这个时候的换乘楼层不能随便被电梯占用,所以在电梯线程的每次循环中,先判断该时刻楼层是否是换乘楼层和在该楼层外面的且需要上该电梯的乘客是否为空,若是如此,则立马离开换乘楼层且进入下一个线程循环。
对同一个电梯井道的两个电梯增加同一个TransferFlag变量,用来实时更新换乘楼层占用情况。在电梯move的时候,先sleep,然后判断是否需要占用换乘楼层或是释放掉换乘楼层的占用。
public void move(int flag) {
try {
sleep((int) (movetime * 1000));
floor = floor + flag;
} catch (InterruptedException e) {
e.printStackTrace();
}
if (/*到达楼层为换乘楼层*/) {
transferFlag.setOccupied();
}
print();
if (/*到达楼层的前一个楼层为换乘楼层*/) {
transferFlag.release();
}
}
private Occupy occupy;
public TransferFlag() {
this.occupy = Occupy.UNOCCUPIED;
}
public synchronized void setOccupied() {
waitRelease();
occupy = Occupy.OCCUPIED;
notifyAll();
}
public synchronized void release() {
this.occupy = Occupy.UNOCCUPIED;
notifyAll();
}
public synchronized void waitRelease() {
notifyAll();
while (occupy == Occupy.OCCUPIED) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
这次作业由于出现了死锁问题,很遗憾没有通过有效性,希望之后能注重oo的理论学习和实践经验的积累,杜绝类似的问题发生叭。
当涉及到多个线程共享一个对象时,就要考虑加锁。锁的选择是多样的,要结合自身需求选择。而加锁要慎重,不能盲目加锁,也尽量避免大范围的加锁,考虑线程与线程之间的关系,以免出现死锁的情况。此外,要善用wait-notify方法,防止CPU轮询。
我认为,一个类之所以能够被单独划分出来成为一个类,主要考虑的是其功能和职责是体现了单一性原则。例如,在这三次作业中,我设计了一个单独的类Advice,专门用来实现电梯的运行策略。如此考虑是因为将来有可能会出现不同电梯的运行策略不同的情况(例如,直达电梯等),用一个专门的类实现策略方便实现统一接口。