文章目录
Unit 2 电梯调度
第五次作业
UML 图
协作图
任务分析
最终目标:将乘客用指定的电梯运达指定的楼层
- 如何组织整体的架构?如何组织各个线程?
- 如何实现对共享对象的保护?
- 电梯如何运行?如何捎带?
总体设计与线程组织
- 整体为“1 + 1 + 6” 线程组织,一个输入线程,一个调度器线程,六个处理请求的电梯线程
- 输入线程接受请求,并将请求加入总请求队列
- 如果输入结束,那么将总请求队列的结束标志置位,结束输入线程
- 调度器线程从总请求队列中取出一个请求,根据其要求的电梯将其分配给对应的电梯请求队列
- 如果总请求队列的结束标志被置位,那么将每个电梯请求队列的结束标志置位,结束调度器线程
- 电梯线程从电梯请求队列中取出请求并处理,被电梯选中的请求将被放入电梯乘客队列,乘客到站则将其从电梯乘客队列中移除
- 如果 电梯请求队列 的结束标志被置位且 电梯乘客队列 为空,那么电梯线程结束。
- 输入线程接受请求,并将请求加入总请求队列
- 输入线程通过 总请求队列 与调度器耦合,调度器管理六个 电梯请求队列 ,通过 电梯请求队列 与电梯耦合
如何实现对共享对象的保护
很明显共享的对象是 请求队列 ,我选择实现一个线程安全的请求队列类。
// RequestQueue,java
public class RequestQueue<T> {
private boolean endTag = false; // 结束标志
private ArrayList<T> requestQueue;
......
public synchronized ArrayList<T> getRequestQueue() {
return requestQueue;
}
public int getSize() {
return requestQueue.size();
}
public synchronized void addRequest(T request) { // 新增一个请求
requestQueue.add(request);
notifyAll();
}
public synchronized void wake() {
notifyAll();
}
public synchronized T getOneRequestAndRemove() {
if (requestQueue.isEmpty() && !endTag) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (requestQueue.isEmpty()) {
return null;
}
T request = requestQueue.get(0);
requestQueue.remove(0);
notifyAll();
return request;
}
public synchronized boolean isEmpty() {
notifyAll();
return requestQueue.isEmpty();
}
public synchronized boolean isEnd() {
notifyAll();
return endTag;
}
public synchronized void setEnd(boolean endTag) {
this.endTag = endTag;
notifyAll();
}
}
但是要注意,请求队列类是线程安全的,但是如果将 请求队列类内部的 ArrayList 取出来操作 ,那一定要在最外边对这个 请求队列上锁,否则可能出现数据竞争导致错误。类似于:
//
synchronized (requests) {
......
requests.getX();
......
}
电梯运行策略
电梯运行策略采取往届学长普遍使用的 LOOK 策略,同时电梯采取运行与策略分离的设计,电梯类内部嵌套策略类和电梯状态类
如 Hyggge 学长所写:
代码复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
MainClass.main(String[]) | 1 | 1 | 2 | 2 |
controller.PersonRequestQueue.PersonRequestQueue() | 0 | 1 | 1 | 1 |
controller.PersonRequestQueue.getMoveDirection(PersonRequest) | 0 | 1 | 1 | 1 |
controller.PersonRequestQueue.getOnePersonAndRemoveNoWait() | 1 | 2 | 1 | 2 |
controller.PersonRequestQueue.getOneRequestByFromFloorAndRemove(int, boolean) | 5 | 4 | 4 | 5 |
controller.PersonRequestQueue.getRequestQueue() | 0 | 1 | 1 | 1 |
controller.PersonRequestQueue.waitRequest() | 1 | 1 | 1 | 2 |
controller.PersonRequestQueue.wake() | 0 | 1 | 1 | 1 |
controller.RequestQueue.RequestQueue() | 0 | 1 | 1 | 1 |
controller.RequestQueue.addRequest(T) | 0 | 1 | 1 | 1 |
controller.RequestQueue.addRequestButNotNotify(T) | 0 | 1 | 1 | 1 |
controller.RequestQueue.getEndTag() | 0 | 1 | 1 | 1 |
controller.RequestQueue.getOneRequestAndRemove() | 5 | 3 | 3 | 5 |
controller.RequestQueue.getRequestQueue() | 0 | 1 | 1 | 1 |
controller.RequestQueue.getSize() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
controller.RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
controller.RequestQueue.wake() | 0 | 1 | 1 | 1 |
controller.Scheduler.Scheduler(RequestQueue<Request>, ArrayList<PersonRequestQueue>, ArrayList<ElevatorStatus>, ArrayList<PersonRequestQueue>) | 0 | 1 | 1 | 1 |
controller.Scheduler.calculatePriority(ElevatorStatus, int) | 1 | 2 | 1 | 2 |
controller.Scheduler.isElevatorResetting() | 3 | 3 | 2 | 3 |
controller.Scheduler.reAddPerson() | 5 | 3 | 3 | 4 |
controller.Scheduler.run() | 19 | 4 | 9 | 11 |
io.InputProcess.InputProcess(RequestQueue<Request>) | 0 | 1 | 1 | 1 |
io.InputProcess.run() | 7 | 3 | 3 | 4 |
servicer.Elevator.Elevator(PersonRequestQueue, int, PersonRequestQueue) | 0 | 1 | 1 | 1 |
servicer.Elevator.checkReset() | 5 | 2 | 3 | 4 |
servicer.Elevator.cleanPassengers() | 4 | 1 | 3 | 4 |
servicer.Elevator.cleanRequests() | 1 | 1 | 2 | 2 |
servicer.Elevator.finishRequestsByToFloorAndRemove(int) | 3 | 1 | 3 | 3 |
servicer.Elevator.getStatus() | 0 | 1 | 1 | 1 |
servicer.Elevator.move() | 3 | 1 | 1 | 3 |
servicer.Elevator.openAndClose() | 8 | 1 | 5 | 6 |
servicer.Elevator.run() | 9 | 4 | 7 | 8 |
servicer.Elevator.toString() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.ElevatorStatus(int, boolean, int, int) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.addOnePerson() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.finishOneRequest() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getCurrentLoadCount() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getFullLoadLimit() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getMoveDirection() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getMoveOneFloorTime() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.isReset() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.resetStatus() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.reverseMoveDirection() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setOver() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setReset(boolean) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setResetInfo(ResetRequest) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.toString() | 0 | 1 | 1 | 1 |
strategy.LookStrategy.LookStrategy(PersonRequestQueue, ArrayList<PersonRequest>, ElevatorStatus) | 0 | 1 | 1 | 1 |
strategy.LookStrategy.canOpenForIn(int, boolean) | 8 | 4 | 4 | 5 |
strategy.LookStrategy.canOpenForOut(int) | 3 | 3 | 2 | 3 |
strategy.LookStrategy.getAdvice(int, boolean) | 13 | 6 | 4 | 7 |
strategy.LookStrategy.getMoveDirection(PersonRequest) | 0 | 1 | 1 | 1 |
strategy.LookStrategy.hasReqInOriginDirection(int, boolean) | 6 | 4 | 5 | 6 |
- 调度器线程各项复杂度均很高,是因为与调度器耦合的类、方法极多
LookStrategy.getAdcive()
方法复杂度高,是因为通过该方法获取对应的电梯下一步运行状态,需要遍历请求队列来判断电梯如何运行,且判断逻辑较复杂。strategy.LookStrategy.hasReqInOriginDirection(int, boolean)
同理。servicer.Elevator.openAndClose()
复杂度较高,因为该方法同时处理进入电梯的请求和已经完成的乘客请求,有多个循环和复杂的判断逻辑
Bug 修复
本次作业没有出现 bug,但是在实现时出现了线程不安全的操作,即从线程安全的类里取出队列,然后对队列操作。
第六次作业
UML 图
协作图
任务分析
- 乘客不指定电梯,需要自行调度
- 新增
RESET
请求
关于 RESET 请求的实现
在 InputHandler
中不区分乘客请求和电梯重置请求,当调度器接收到电梯重置请求,则将对应电梯的 ElevatorStatus
设为重置模式,在 Elevator
中处理重置请求。
-
Elevator
如果接受到重置请求,则立即停止运行。同时将电梯内原有的乘客放出,将未到目的地的乘客放回总请求队列;将已分配到电梯的请求取消,放回总请求队列。也就是将RESET
视为电梯运行时的一个动作。// Elevator.java public void checkReset() { if (status.isReset()) { if (!passengers.isEmpty()) { cleanPassengers(); } TimableOutput.println("RESET_BEGIN-" + elevatorId); cleanRequests(); ...... // 睡 1200ms status.resetStatus(); TimableOutput.println("RESET_END-" + elevatorId); } }
-
Scheduler
每次先将因RESET
而退出的乘客重新加入总请求队列,然后执行正常工作// Scheduler.java public void run() { while (true) { reAddPerson(); // 退出电梯的请求和乘客重新放回总请求队列 if (totalQueue.isEmpty() && totalQueue.isEnd() && !isElevatorResetting()) { ...... // 设置结束标志,当有某个电梯处于重置状态时不能结束 return; } Request request = totalQueue.getOneRequestAndRemove(); if (request == null) { continue; } if (request instanceof ResetRequest) { ElevatorStatus elevatorStatus = runningStates. get(((ResetRequest) request).getElevatorId() - 1); elevatorStatus.setResetInfo((ResetRequest) request); elevatorStatus.setReset(true); processingQueues.get(((ResetRequest) request).getElevatorId() - 1).wake(); } else if (request instanceof PersonRequest) { ...... // 进行乘客调度 } ...... } }
关于人员调度
-
影响调度的因素:
- 是否处于
Reset
状态 - 电梯同时处理多条请求的能力——电梯最大载客量
- 电梯运行速度
- 目前该电梯候乘人数
- 是否处于
-
为每个电梯设置一个 “优先级”,将请求分给优先级最高的电梯。若电梯优先级相同,则选择序号最小的电梯
-
电梯优先级的计算:
P r i o r i t y = log ( 满载人数 电梯移动速度 ) ∗ exp ( − 当前乘客人数 − 当前等待人数 ) Priority\ =\ \log({{满载人数}\ \over\ {电梯移动速度} })\ *\ \exp({-当前乘客人数\ -当前等待人数 }) Priority = log( 电梯移动速度满载人数 ) ∗ exp(−当前乘客人数 −当前等待人数) -
如果电梯处于重置状态,则其优先级为 0
-
如果所有电梯都处于重置状态,则调度器休眠 600ms 后再调度
-
代码复杂度分析
Method | CogC | ev(G) | iv(G) | v(G) |
---|---|---|---|---|
MainClass.main(String[]) | 1 | 1 | 2 | 2 |
controller.RequestQueue.RequestQueue() | 0 | 1 | 1 | 1 |
controller.RequestQueue.addRequest(T) | 0 | 1 | 1 | 1 |
controller.RequestQueue.getMoveDirection(PersonRequest) | 0 | 1 | 1 | 1 |
controller.RequestQueue.getOnePersonAndRemoveNoWait() | 1 | 2 | 1 | 2 |
controller.RequestQueue.getOneRequestAndRemove() | 5 | 3 | 3 | 5 |
controller.RequestQueue.getOneRequestByFromFloorAndRemove(int, boolean) | 5 | 4 | 4 | 5 |
controller.RequestQueue.getRequestQueue() | 0 | 1 | 1 | 1 |
controller.RequestQueue.getSize() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
controller.RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
controller.RequestQueue.waitRequest() | 1 | 1 | 1 | 2 |
controller.RequestQueue.wake() | 0 | 1 | 1 | 1 |
controller.Scheduler.Scheduler(RequestQueue<Request>, ArrayList<RequestQueue<PersonRequest>>, ArrayList<ElevatorStatus>, ArrayList<RequestQueue<PersonRequest>>) | 0 | 1 | 1 | 1 |
controller.Scheduler.calculatePriority(ElevatorStatus, int) | 1 | 2 | 1 | 2 |
controller.Scheduler.getResetNum() | 3 | 1 | 2 | 3 |
controller.Scheduler.isElevatorAllResetting() | 3 | 3 | 2 | 3 |
controller.Scheduler.isElevatorResetting() | 3 | 3 | 2 | 3 |
controller.Scheduler.reAddPerson() | 5 | 3 | 3 | 4 |
controller.Scheduler.run() | 33 | 9 | 13 | 16 |
io.InputProcess.InputProcess(RequestQueue<Request>) | 0 | 1 | 1 | 1 |
io.InputProcess.run() | 7 | 3 | 3 | 4 |
servicer.Elevator.Elevator(RequestQueue<PersonRequest>, int, RequestQueue<PersonRequest>) | 0 | 1 | 1 | 1 |
servicer.Elevator.checkReset() | 5 | 2 | 3 | 4 |
servicer.Elevator.cleanPassengers() | 5 | 1 | 4 | 5 |
servicer.Elevator.cleanRequests() | 1 | 1 | 2 | 2 |
servicer.Elevator.finishRequestsByToFloorAndRemove(int) | 3 | 1 | 3 | 3 |
servicer.Elevator.getStatus() | 0 | 1 | 1 | 1 |
servicer.Elevator.move() | 3 | 1 | 1 | 3 |
servicer.Elevator.openAndClose() | 8 | 1 | 5 | 6 |
servicer.Elevator.run() | 7 | 3 | 7 | 7 |
servicer.Elevator.toString() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.ElevatorStatus(int, boolean, int, int) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.addOnePerson() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.finishOneRequest() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getCurrentLoadCount() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getFullLoadLimit() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getMoveDirection() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getMoveOneFloorTime() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.isReset() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.resetStatus() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.reverseMoveDirection() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setOver() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setReset(boolean) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setResetInfo(ResetRequest) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.toString() | 0 | 1 | 1 | 1 |
strategy.LookStrategy.LookStrategy(RequestQueue<PersonRequest>, ArrayList<PersonRequest>, ElevatorStatus, RequestQueue<PersonRequest>) | 0 | 1 | 1 | 1 |
strategy.LookStrategy.canOpenForIn(int, boolean) | 8 | 4 | 4 | 5 |
strategy.LookStrategy.canOpenForOut(int) | 3 | 3 | 2 | 3 |
strategy.LookStrategy.getAdvice(int, boolean) | 14 | 6 | 5 | 8 |
strategy.LookStrategy.getMoveDirection(PersonRequest) | 0 | 1 | 1 | 1 |
strategy.LookStrategy.hasReqInOriginDirection(int, boolean) | 6 | 4 | 5 | 6 |
controller.RequestQueue.getOneRequestByFromFloorAndRemove(int, boolean)
复杂度超标,是因为要根据出发楼层和移动方向选取请求并移除,判断逻辑较为复杂controller.Scheduler.run()
复杂度极高,因为加入RESRT
请求后出现了中途换乘,同时新增了人员调度任务。在第七次作业做了部分优化
Bug 修复
在本次作业中出现了 RTLE
bug,主要原因是当所有电梯都处于 RESET
状态的处理方式不对,出现了选中的 ElevatorId
为 -1
的错误,进而导致线程无法正常退出进而 RTLE
。
第七次作业
UML 图
协作图
任务分析
- 新增双轿厢电梯的
RESET
- 如何创建双轿厢电梯?
- 如何调整调度策略?
- 双轿厢电梯如何防撞?
- 线程退出的条件?
创建双轿厢电梯
我选择在 Scheduler
中组建双轿厢电梯。当收到双轿厢重置请求时,将该请求发给对应的电梯,该电梯进行双轿厢重置工作(也就是将属性改为 ELEVATOR_TYPE_D
);同时组建双轿厢电梯,新建双轿厢电梯请求 HashMap ,将双轿厢的部分属性(如电梯请求队列)放入其中。
// Scheduler.java
public void processDoubleCarReset(DoubleCarResetRequest request) {
int elevatorId = request.getElevatorId();
ElevatorStatus elevatorStatus = runningStates.get(elevatorId - 1);
elevatorStatus.setResetInfo(request);
elevatorStatus.setTransferFloor(request.getTransferFloor());
elevatorStatus.setReset(2); // 双轿厢重置标志
processingQueues.get(elevatorId - 1).wake();
transferFloor.put(elevatorId, request.getTransferFloor());
createDoubleCarElevator(elevatorId, occupies.get(elevatorId), request);
}
private void createDoubleCarElevator(int elevatorId, Flag occupied,
DoubleCarResetRequest request) {
RequestQueue<PersonRequest> newQueue = new RequestQueue<>(); // 待处理队伍
doubleCarQueues.put(elevatorId, newQueue);
RequestQueue<PersonRequest> newExitHalfway = new RequestQueue<>();
ElevatorStatus elevatorStatus = new ElevatorStatus((int)(request.getSpeed() * 1000), true,
request.getCapacity(), 0);
elevatorStatus.setTransferFloor(request.getTransferFloor());
ArrayList<PersonRequest> passengers = new ArrayList<>();
passengerQueues.add(passengers);
Elevator elevator = new Elevator(newQueue, elevatorId, passengers, occupied, totalQueue, count);
elevator.setName(elevatorId + "-B");
elevator.setDoubleCarStatus(elevatorStatus);
elevator.start();
}
关于调度策略
大道至简,Random
策略不失为一种好的选择。由 Random
生成随机数作为选中的电梯的序号,然后将该请求分给该电梯。
关于请求的分配:
在Scheduler
中设置了一个存放换乘楼层的HashMap,Key 对应电梯编号,Value 对应换乘楼层。在 Scheduler
初始化时,将所有Value 初始化为 0,代表不是双轿厢电梯。
-
如果分到的电梯不是双轿厢电梯
- 直接将指令分给它
-
如果分到的电梯是双轿厢电梯
- 出发楼层 > 换乘楼层
- 分到 B 梯
- 出发楼层 < 换乘楼层
- 分到 A 梯
- 出发楼层 == 换乘楼层
- 如果目的楼层 > 换乘楼层
- 分到 B 梯
- 如果目的楼层 > 换乘楼层
- 分到 A 梯
- 如果目的楼层 > 换乘楼层
- 出发楼层 > 换乘楼层
-
// Scheduler.java public void dispatchPersonRequest(int elevatorId, PersonRequest request) { if (transferFloor.get(elevatorId) == 0) { processingQueues.get(elevatorId - 1).addRequest(request); TimableOutput.println("RECEIVE-" + request.getPersonId() + "-" + elevatorId); } else { int transFloor = transferFloor.get(elevatorId); int fromFloor = request.getFromFloor(); if (fromFloor < transFloor) { processingQueues.get(elevatorId - 1).addRequest(request); TimableOutput.println("RECEIVE-" + request.getPersonId() + "-" + elevatorId + "-A"); } else if (fromFloor > transFloor) { doubleCarQueues.get(elevatorId).addRequest(request); TimableOutput.println("RECEIVE-" + request.getPersonId() + "-" + elevatorId + "-B"); } else { int toFloor = request.getToFloor(); if (toFloor < transFloor) { processingQueues.get(elevatorId - 1).addRequest(request); TimableOutput.println("RECEIVE-" + request.getPersonId() + "-" + elevatorId + "-A"); } else { doubleCarQueues.get(elevatorId).addRequest(request); TimableOutput.println("RECEIVE-" + request.getPersonId() + "-" + elevatorId + "-B"); } } } }
关于防止电梯碰撞
采用的方法是评论区姜涵章同学提供的思路,为每部电梯设置一个标志位,标志位没有被置位时电梯可以进入换乘楼层,若某个电梯进入换乘楼层则置位,此时另一个电梯就无法进入了。
// Flag.java
public class Flag {
enum State { OCCUPIED, UNOCCUPIED }
private State state;
public Flag() {
this.state = State.UNOCCUPIED;
}
public synchronized void setOccupied() {
waitRelease();
state = State.OCCUPIED;
notifyAll();
}
public synchronized void setRelease() {
this.state = State.UNOCCUPIED;
notifyAll();
}
private synchronized void waitRelease() {
notifyAll();
while (state == State.OCCUPIED) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
在电梯进入换乘楼层是将标志位置位,离开时取消标志位的置位。
// Elevator.java
public void move() {
try {
this.sleep(status.getMoveOneFloorTime());
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
int moveDir = 0;
if (status.getMoveDirection()) {
curFloor++;
moveDir = 1;
} else {
curFloor--;
moveDir = -1;
}
if (elevatorType == ELEVATOR_TYPE_D && curFloor == status.getTransferFloor()) {
synchronized (occupied) {
occupied.setOccupied();
}
}
TimableOutput.println("ARRIVE-" + curFloor + "-" + getName());
if (elevatorType == ELEVATOR_TYPE_D &&
curFloor - moveDir == status.getTransferFloor()) {
synchronized (occupied) {
occupied.setRelease();
}
}
}
关于线程的结束
- 调度器线程不能先于电梯线程结束。如果电梯还没结束,调度器已经结束,可能出现在换乘电梯里的乘客无法被二次接受(因为本次出现双轿厢电梯,对于普通电梯来说,如果之后没有
RESET
请求,那么电梯里的乘客一定能被送到目的地;而双轿厢电梯则不一定,可能需要换乘) - 电梯的结束受调度器的限制:如果调度器里还有请求,那么电梯不能结束
- 电梯结束的条件
- 当前电梯里没有乘客
- 没有当前电梯的请求
- 总请求表为空
- 调度器结束条件:
- 输入结束
- 总请求表为空
- 所有乘客请求得到处理
我选择实现一个线程安全的计数器,每当收到一个乘客请求时就自增,每当完成一个乘客请求就自减。当计数器为零且输入结束时,可以保证没有未完成的乘客请求。
public class RequestCount {
private int cnt;
public RequestCount() {
this.cnt = 0;
}
public synchronized void addCnt() {
++cnt;
notifyAll();
}
public synchronized void finish() {
--cnt;
notifyAll();
}
public synchronized void finish(int num) {
cnt = cnt - num;
notifyAll();
}
public synchronized int getCnt() {
// System.out.println("cnt: " + cnt);
return cnt;
}
}
Scheduler
的退出逻辑如下
// Shceduler.java
public void run() {
while (true) {
if (count.getCnt() == 0) {
totalQueue.setRunningEnd(true);
}
if (totalQueue.isEnd() && !isElevatorResetting() && count.getCnt() == 0) {
for (int i = 0; i < processingQueues.size(); i++) {
processingQueues.get(i).setEnd(true);
runningStates.get(i).setOver();
}
for (RequestQueue<PersonRequest> doubleCarQueue : doubleCarQueues.values()) {
doubleCarQueue.setEnd(true);
}
return;
}
......
}
}
Bug 修复
这次出现了 RTLE bug,没找到原因,但是将“六个电梯全部处于 RESET 时 Scheduler 睡眠1200ms” 改为睡眠 600ms 就能够通过了,可能是睡眠时间太长导致线程无法退出?
代码复杂度分析
MainClass.main(String[]) | 1 | 1 | 2 | 2 |
---|---|---|---|---|
controller.RequestCount.RequestCount() | 0 | 1 | 1 | 1 |
controller.RequestCount.addCnt() | 0 | 1 | 1 | 1 |
controller.RequestCount.finish() | 0 | 1 | 1 | 1 |
controller.RequestCount.finish(int) | 0 | 1 | 1 | 1 |
controller.RequestCount.getCnt() | 0 | 1 | 1 | 1 |
controller.RequestQueue.RequestQueue() | 0 | 1 | 1 | 1 |
controller.RequestQueue.addRequest(T) | 0 | 1 | 1 | 1 |
controller.RequestQueue.getMoveDirection(PersonRequest) | 0 | 1 | 1 | 1 |
controller.RequestQueue.getOnePersonAndRemoveNoWait() | 1 | 2 | 1 | 2 |
controller.RequestQueue.getOneRequestAndRemove() | 5 | 3 | 3 | 5 |
controller.RequestQueue.getOneRequestByFromFloorAndRemove(int, boolean) | 5 | 4 | 4 | 5 |
controller.RequestQueue.getOneTotalRequestAndRemove() | 5 | 3 | 3 | 5 |
controller.RequestQueue.getRequestQueue() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isEmpty() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isEmptyNoNotify() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isEnd() | 0 | 1 | 1 | 1 |
controller.RequestQueue.isRunningEnd() | 0 | 1 | 1 | 1 |
controller.RequestQueue.setEnd(boolean) | 0 | 1 | 1 | 1 |
controller.RequestQueue.setRunningEnd(boolean) | 0 | 1 | 1 | 1 |
controller.RequestQueue.waitRequest() | 1 | 1 | 1 | 2 |
controller.RequestQueue.wake() | 0 | 1 | 1 | 1 |
controller.Scheduler.Scheduler(RequestQueue<Request>, ArrayList<RequestQueue<PersonRequest>>, ArrayList<ElevatorStatus>, ArrayList<ArrayList<PersonRequest>>, RequestCount, HashMap<Integer, Flag>) | 1 | 1 | 2 | 2 |
controller.Scheduler.createDoubleCarElevator(int, Flag, DoubleCarResetRequest) | 0 | 1 | 1 | 1 |
controller.Scheduler.dispatchPersonRequest(int, PersonRequest) | 10 | 1 | 5 | 5 |
controller.Scheduler.getResetNum() | 3 | 1 | 2 | 3 |
controller.Scheduler.isElevatorResetting() | 3 | 3 | 2 | 3 |
controller.Scheduler.isElevatorRunning() | 9 | 7 | 4 | 7 |
controller.Scheduler.processDoubleCarReset(DoubleCarResetRequest) | 0 | 1 | 1 | 1 |
controller.Scheduler.processNormalReset(NormalResetRequest) | 0 | 1 | 1 | 1 |
controller.Scheduler.run() | 31 | 8 | 13 | 15 |
io.InputProcess.InputProcess(RequestQueue<Request>, RequestCount) | 0 | 1 | 1 | 1 |
io.InputProcess.run() | 10 | 3 | 4 | 5 |
servicer.Elevator.Elevator(RequestQueue<PersonRequest>, int, ArrayList<PersonRequest>, Flag, RequestQueue<Request>, RequestCount) | 0 | 1 | 1 | 1 |
servicer.Elevator.checkReset() | 10 | 3 | 4 | 6 |
servicer.Elevator.cleanPassengers() | 4 | 1 | 4 | 4 |
servicer.Elevator.cleanRequests() | 1 | 1 | 2 | 2 |
servicer.Elevator.finishRequestsByToFloorAndRemove(int) | 3 | 1 | 3 | 3 |
servicer.Elevator.getStatus() | 0 | 1 | 1 | 1 |
servicer.Elevator.move() | 7 | 1 | 5 | 7 |
servicer.Elevator.openAndClose() | 1 | 1 | 1 | 2 |
servicer.Elevator.requestEnterByFromFloor() | 7 | 1 | 5 | 5 |
servicer.Elevator.run() | 14 | 7 | 11 | 11 |
servicer.Elevator.setDoubleCarStatus(ElevatorStatus) | 0 | 1 | 1 | 1 |
servicer.Elevator.setNormalStatus() | 0 | 1 | 1 | 1 |
servicer.Elevator.toString() | 0 | 1 | 1 | 1 |
servicer.Elevator.transfer() | 2 | 1 | 2 | 3 |
servicer.ElevatorStatus.ElevatorStatus(int, boolean, int, int) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.addOnePerson() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.finishOneRequest() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getFullLoadLimit() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getMoveDirection() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getMoveOneFloorTime() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getReset() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.getTransferFloor() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.resetStatus() | 2 | 1 | 2 | 2 |
servicer.ElevatorStatus.reverseMoveDirection() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setMoveDirection(boolean) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setOver() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setReset(int) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setResetInfo(ResetRequest) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.setTransferFloor(int) | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.toString() | 0 | 1 | 1 | 1 |
servicer.ElevatorStatus.wake() | 0 | 1 | 1 | 1 |
servicer.Flag.Flag() | 0 | 1 | 1 | 1 |
servicer.Flag.setOccupied() | 0 | 1 | 1 | 1 |
servicer.Flag.setRelease() | 0 | 1 | 1 | 1 |
servicer.Flag.waitRelease() | 3 | 2 | 2 | 3 |
strategy.LookStrategy.LookStrategy(RequestQueue<PersonRequest>, ArrayList<PersonRequest>, ElevatorStatus) | 0 | 1 | 1 | 1 |
strategy.LookStrategy.canOpenForIn(int, boolean) | 8 | 4 | 4 | 5 |
strategy.LookStrategy.canOpenForOut(int) | 3 | 3 | 2 | 3 |
strategy.LookStrategy.getAdvice(int, boolean) | 14 | 7 | 4 | 8 |
strategy.LookStrategy.getMoveDirection(PersonRequest) | 0 | 1 | 1 | 1 |
strategy.LookStrategy.hasReqInOriginDirection(int, boolean) | 6 | 4 | 5 | 6 |
controller.Scheduler.run()
相较之前降低了一点复杂度,但是由于双轿厢电梯的加入导致逻辑进一步复杂,所以复杂度仍然居高不下。在研讨课中荣文戈老师提到在每个电梯设置小调度器,中央调度器负责管理小调度器,这确实是降低调度器复杂度的好办法(毕竟所有事都交给总调度器处理。不符合面向对象的设计哲学),可惜在自己设计时没有想到
Bug 与 DeBug
对我来说,最容易出现的 bug 有两类:一类是没有做好线程安全,出现了数据竞争导致的错误;另一类是奇奇怪怪原因导致的 RTLE 与 CTLE。
线程安全
多线程最为关键的就是保证线程安全。对共享资源有读有写时一定要注意确保只有一个线程能够访问该资源。在第一次作业中我实现了
线程安全的”请求队列“类,然后取出该类中的属性——某个ArrayList,对取出的队列没有做好线程安全就进行操作,导致出现了 bug。
一种简单暴力不优雅的做法是,将所有出现的共享资源全部上锁,直接对方法加锁。优雅的做法是使用读写锁,仔细判断是读还是写。但是读写锁可能出现意想不到的 bug ,同时逻辑复杂。
线程安全出现 bug时,可以通过“println大法”:在访问和修改共享变量的方法里输出** “是哪个线程要访问(修改)哪个共享变量” , “是哪个线程完成了访问(修改)哪个共享变量” **,当出现某个线程还未完成访问或修改时,另一个线程已经开始访问了,就说明出现了线程不安全问题。
RTLE 与 CTLE
助教提供的解决 CTLE bug 的方法令我十分受用,我主要想谈谈 RTLE bug。我遇到的 RTLE 大多是因为某个线程没有按时退出。对于这类 bug,我会在所有 wait()
之前输出”某个线程将要 wait“,在被唤醒后输出”某个线程被唤醒“,这样就可以确定具体的位置。
最为困难的是确定 为什么该线程没有退出,这部分只能进行代码逻辑复查。比如:如果在调度器中的逻辑是:”输入结束并且总请求队列为空,那么调度器线程结束“,如果此时刚好有一个双轿厢电梯在运行,并且电梯内的某个请求无法直达,需要换乘,这样就会导致换乘时该乘客无法被再次调度,滞留在总请求表中,使得电梯线程无法退出。
心得体会
关于线程安全
见 “Bug 与 Debug” 部分
关于层次化设计
好的层次化设计可以有效减少 bug,使代码易于理解、易于阅读、易于更新迭代。与其他同学比起来我的架构简直难以形容,我的每次迭代都会被之前的架构恶心到,然后将它修改为这次还不错、下次作业更恶心的架构。
每次迭代我都会想:为什么这个模块耦合度如此之高,牵一发而动全身;这段代码负责如此多的功能、能否简化其职责?与先导课相比,目前的设计已经有了极大的进步,但是还不够。在设计时应该始终遵循高内聚、低耦合的理念,追求职责的单一化,以及一种模块负责一种功能的设计,架构设计的优化永无止尽。
后记
第二单元做得十分艰难,有三个原因:
- 第一次接触多线程,对于线程同步互斥不熟悉
- 多线程出现问题,本地难以稳定复现。你知道有 bug,但是复现不出来,单纯代码走读效果不佳,这简直是一种酷刑
- 思维的变化,由串行编程变为并行编程
虽然磕磕绊绊,但是成功完成了三次作业,不禁心生欢喜。想起了翁恺老师的的一句话,”计算机是人造出来的,它的一切都是人创造的。你不理解,只是暂时的。时间足够,没有我们不会的东西“。
我们始终在路上。
在被唤醒后输出”某个线程被唤醒“,这样就可以确定具体的位置。
最为困难的是确定 为什么该线程没有退出,这部分只能进行代码逻辑复查。比如:如果在调度器中的逻辑是:“输入结束并且总请求队列为空,那么调度器线程结束”,如果此时刚好有一个双轿厢电梯在运行,并且电梯内的某个请求无法直达,需要换乘,这样就会导致换乘时该乘客无法被再次调度,滞留在总请求表中,使得电梯线程无法退出。
心得体会
关于线程安全
见 “Bug 与 Debug” 部分
关于层次化设计
好的层次化设计可以有效减少 bug,使代码易于理解、易于阅读、易于更新迭代。与其他同学比起来我的架构简直难以形容,我的每次迭代都会被之前的架构恶心到,然后将它修改为这次还不错、下次作业更恶心的架构。
每次迭代我都会想:为什么这个模块耦合度如此之高,牵一发而动全身;这段代码负责如此多的功能、能否简化其职责?与先导课相比,目前的设计已经有了极大的进步,但是还不够。在设计时应该始终遵循高内聚、低耦合的理念,追求职责的单一化,以及一种模块负责一种功能的设计,架构设计的优化永无止尽。
后记
第二单元做得十分艰难,有三个原因:
- 第一次接触多线程,对于线程同步互斥不熟悉
- 多线程出现问题,本地难以稳定复现。你知道有 bug,但是复现不出来,单纯代码走读效果不佳,这简直是一种酷刑
- 思维的变化,由串行编程变为并行编程
虽然磕磕绊绊,但是成功完成了三次作业,不禁心生欢喜。想起了翁恺老师的的一句话,“计算机是人造出来的,它的一切都是人创造的。你不理解,只是暂时的。时间足够,没有我们不会的东西”。
我们始终在路上。