OO 第二单元总结
博客撰写基本要求:
OO 第二单元总结
概述
本单元设定了一个电梯接送乘客、模拟电梯重置和"分身"的场景,涉及多线程、线程安全、共享资源及其锁的使用等许多问题,相比于上个单元,在架构和实现方式、debug、测试和 bug 复现、性能优化上,都有许多不同.
回顾一下这个单元的三次作业,可以发现是一个逐渐深入,愈发复杂的过程.
hw5 是在请求中指定了电梯分配,主要目的是让同学们对线程的基本操作有所了解,并解决电梯对于一个队列应当如何接送乘客的问题,为这个单元后面多电梯调度打好框架。
hw6 中取消了请求中对电梯分配的指定,并新增了基础的重置操作,主要解决多电梯调度问题.
hw7 中又新增了一个双轿厢电梯重置请求,使每一个"电梯槽"里面也应该有两个线程,并且需要解决在换乘层上下两个电梯冲突的问题。
作业架构迭代
hw5
本次作业中, 我只开了两种线程, InputThread 和 Elevator. 因为电梯都分配好了, 没必要写调度器.
InputThread 直接将读到的请求分配给相应电梯上的 requestTable
我的 Scheduler 类事实上并不是调度器,而是一个内部策略类. 所以后面我直接将它重命名为 Stratrgy 了.
策略采用 Look 算法, 简单有效.
在 MainClass 中创建所有线程一起 start.
这样,我的结构第一次比较简单,甚至简陋,后面改起来就比较麻烦
hw6
本次作业
uml
时序图
本次作业新增 reset, 并且需要完成调度器设计.
吃了第一次的亏, 重构过程一波三折.
重命名原来的 Scheduler 为 Strategy, 并新增线程 Scheduler 用于调度.
本来 waitQueue 想要复用 requestTable, 但是发现 resetRequest 放进 requestTable 并不合适. 所以又新建了一个类. 如果最开始设计好,完全可以写成继承.
关于 reset: resetRequest 与 personRequest 一同进入 waitQueue, 然后由调度器分配给相应的电梯中的 requestTable. 我一直对此有所担心, 因为 resetRequest 无法在 accept 的第一时间给到电梯. 所以我是让电梯收到 reset 就直接就近停下, reset. 并不贪图再走 1 层.
reset 接受后到完成 过程中, 置位 isReset, 在这个过程中, 依然会向其分配请求, 但是并不输出 -- 而是将其分配到一个缓冲队列当中, 等到 reset 完成时再正式 receive.
reset 过程要开门下客, 下来的请求需打回重新调度. 那么 inputThread 结束, 就不能直接作为 Scheduler 结束 的标志, 而应该与上所有 的电梯都没有正在 reset.
- 调度策略
我一开始考虑的调度策略是选取几个参数比较, 但是写出来以后性能一言难尽, 所以还是直接选择了 random.
hw 7
我中测没过, 很痛苦. 但还是大概讲讲我的思路吧
新增兄弟电梯.
-
- 调度策略: 依然是 random. 如果有兄弟电梯, 则一对电梯 A 和 B 共享 1/6 的概率:
具体而言是这样:
private Elevator randomElevator(int fromFloor) {
int randomNumber = (int) (Math.random() * 6) + 1;
Elevator elevatorA;
Elevator elevatorB;
synchronized (elevators) {
elevatorA = elevators.get(randomNumber);
elevatorB = elevators.get(randomNumber + 6);
if (elevatorA.isDc()) {
if (fromFloor < elevatorA.getTransFloor()) {
return elevatorA;
} else {
return elevatorB;
}
}
}
return elevatorA;
}
- 关于换乘层:
有一个很简单但是我一开始没有注意到的问题: 到达换成层, 所有的乘客都要下车, 下来以后, 也要回到 waitQueue 重新分配.
这一点表明, Scheduler 的结束条件应当再加上一条,也就是没有电梯正在换乘层下客.
我虽然后面想到这一点, 但是对其的解决不够彻底, 出现了好多乘客下去以后, 还没有被重新分配, 就已经结束了线程, 有时还会出现 tle 的情况(死锁).
此外我还粗心大意地将换乘层下车的乘客请求原封不动地打回 waitQueue, 忘记用 curFloor 代替 fromFloor 了.
- 关于分裂时线程的启动:
在 Elevator 中 将自己的 isA, isDc 等属性改变, 变成 elevatorA;然后, 实例化兄弟电梯 B, 最后调用 Scheduler 的addElevator方法, 在 addElevator 方法里 start 新的电梯线程.
这里涉及 elevators 的线程安全问题, 所以我就开始疯狂打补丁, 在使用 elevators 的地方 加锁.
其它一些关键点
如何实现双轿厢的两个轿厢不碰撞
感谢讨论区佬的课堂, 直接学习下来. 用一个 Flag 信号量, 保证兄弟电梯不会同时进入换乘层.
PV 大法真万能.
Elevator.java
private void move() {
try {
sleep(moveTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (isDc && currentFloor + direction == transFloor) {
occupied.waitRelease();
}
currentFloor += direction;
if (isDc && currentFloor == transFloor) {
occupied.setOccupied();
}
OutputHandler.arrive(currentFloor, fullId(), elevatorId);
if (isDc && currentFloor - direction == transFloor) {
occupied.release();
}
// Arrive
}
此外还要考虑"占着内个不内个"的问题, 也就是说, 当一个电梯在换乘层时,不能随随便便就停下,甚至结束. 应当尽快驶离换乘层腾出位置来.
Strategy.java
// 在换乘层,赶紧离开
if (!elevator.isDc() && passengerCount != 0 || elevator.isDc() && curFloor == transFloor &&
Utils.sameDirection(transFloor, elevator.isA() ? minFloor : maxFloor, direction)) {
return Advice.MOVE;
}
锁
先直接来一个汇总:
46 个结果 - 6 文件
src\Elevator.java:
92
93: public synchronized boolean isDc() {
94 return isDc;
96
97: public synchronized void setDc(boolean dc) {
98 isDc = dc;
109
110: public synchronized int getCapacity() {
111 return capacity;
121
122: public synchronized boolean isReseting() {
123 notifyAll();
126
127: public synchronized void setReseting(boolean reseting) {
128 this.reseting = reseting;
129 notifyAll();
130: synchronized (requestTable) {
131 requestTable.notifyAll();
134
135: public synchronized boolean isToBeReset() {
136 notifyAll();
139
140: public synchronized void setToBeReset(ResetRequest resetRequest, boolean toBeReset) {
141 this.toBeReset = toBeReset;
143 notifyAll();
144: synchronized (requestTable) {
145 requestTable.notifyAll();
148
149: public synchronized Advice getCurState() {
150 notifyAll();
160 while (true) {
161: synchronized (this) {
162 curState = Strategy.getAdvice(this);
187
188: public synchronized boolean isOpening() {
189 return curState == Advice.OPEN && isDc && currentFloor == transFloor;
219 }
220: synchronized (waitQueue) {
221 waitQueue.notifyAll();
303
304: synchronized (waitQueue) {
305 waitQueue.notifyAll();
330 }
331: synchronized (waitQueue) {
332 waitQueue.notifyAll();
src\Flag.java:
7
8: public synchronized void setOccupied() {
9 waitRelease();
13
14: public synchronized void release() {
15 this.state = State.UNOCCUPIED;
18
19: public synchronized void waitRelease() {
20 notifyAll();
src\RequestTable.java:
29
30: public synchronized HashMap<Integer, ArrayList<PersonRequest>> getRequestMap() {
31 return requestMap;
33
34: public synchronized void addRequest(PersonRequest request) {
35 requestMap.get(request.getFromFloor()).add(request);
39
40: public synchronized boolean isOver() {
41 notifyAll();
44
45: public synchronized void setOver(boolean isOver) {
46 this.isOver = isOver;
49
50: public synchronized boolean isEmpty() {
51 notifyAll();
55
56: public synchronized void waitRequest() {
57 if (!isOver && isEmpty()) {
75
76: public synchronized ArrayList<PersonRequest> deleteRequest(int currentFloor,
77 int direction,
101
102: public synchronized ArrayList<PersonRequest> deleteAllRequests() {
103
113
114: public synchronized int getRequestCount() {
115 return requestCount;
src\Scheduler.java:
18 public void addElevator(Elevator elevator) {
19: synchronized (elevators) {
20 elevators.put(elevator.getElevatorId(), elevator);
37 boolean b;
38: synchronized (elevators) {
39 b = waitQueue.isInputOver() &&
44 if (b) {
45: synchronized (elevators) {
46 elevators.values().forEach(elevator -> elevator.setOver(true));
55 }
56: synchronized (waitQueue) {
57 try {
76 ((DoubleCarResetRequest) request).getElevatorId();
77: synchronized (elevators) {
78 elevators.get(resetId).setToBeReset((ResetRequest) request, true);
93 Elevator elevatorB;
94: synchronized (elevators) {
95 elevatorA = elevators.get(randomNumber);
129 RequestTable waitList1 = elevator.getRequestTable();
130: synchronized (waitList1) {
131 if (Utils.sameDirection(from, to, direction) && passengerCount < capacity &&
162 RequestTable waitList1 = elevator.getRequestTable();
163: synchronized (waitList1) {
164 return Utils.sameDirection(from, to, direction) && passengerCount < capacity &&
200 int sum = Integer.MAX_VALUE;
201: synchronized (elevators) {
202 for (Elevator elevator : elevators.values()) {
204 RequestTable waitList1 = elevator.getRequestTable();
205: synchronized (waitList1) {
206 int outCount = waitList1.getRequestCount();
217 public Elevator getSibling(int id) {
218: synchronized (elevators) {
219 return elevators.get((id + 6) % 12);
src\Strategy.java:
85 // "顺路就捎":这层有人想去的方向与运动方向相同
86: synchronized (waitQueue) {
87 return haveSpace && waitQueue.stream().anyMatch(personRequest -> Utils.sameDirection(
113 requestMap = requestTable.getRequestMap();
114: synchronized (requestMap) {
115 return requestMap.keySet().stream()
src\WaitQueue.java:
13
14: public synchronized void addRequest(Request request) {
15 requests.add(request);
18
19: public synchronized Request getOneRequestAndRemove() {
20 if (requests.isEmpty()) {
28
29: public synchronized boolean isInputOver() {
30 notifyAll();
33
34: public synchronized void setInputOver(boolean isOver) {
35 this.inputOver = isOver;
38
39: public synchronized ArrayList<Request> getRequests() {
40 notifyAll();
43
44: public synchronized boolean isEmpty() {
45 notifyAll();
可以看到, 我大部分锁都加在了方法上, 主要是对各种托盘的存取方法都加了锁.
但是我还是有许多地方未加以思考就滥用了锁, 导致经常出现死锁.
有几个比较失败的加锁就是对 elevator 加的锁, 这些主要是用来判断 Scheduler 结束条件的.
测试, bug 与 debug
打表的心得:
public static void arrive(int floor, String fullId, int elevatorId) {
TimableOutput.println(String.format(
"%sARRIVE-%d-%s",
Utils.getIndent(elevatorId),
floor,
fullId));
}
根据 id 将输出进行缩进, 不同电梯一眼就能分开.
最大的败笔
Strategy 控制结构复杂, 导致本不是重点的 Strategy 在最后一次作业频繁出错.
if- else 非常混乱,这里一个特判,那里一个特判.
这时候就羡慕状态机了.


