BUAA OO 2022 第二单元总结
OO第二单元总结
1. 第一次作业
1.1 需求分析:
需求:
- A-E五栋楼中初始均有一部电梯,可达1-10楼,乘客不许跨楼座。
- 要求使用
wait
,notifyAll
的形式控制电梯
整体设计:
- 参考实验1中的代码进行设计,实验一中代码过多的无意义
notifyAll()
也成功在作业2中狠狠坑了我一把.(流汗黄豆)
1.2 同步块设计
-
仅在请求队列和输出中加入了锁
-
//请求队列类 RequestQueue 中的所有方法均加入 synchronized 用以控制线程同步 public synchronized void addRequest(Request request) { requests.add(request); length++; notifyAll(); } synchronized (TimableOutput.class) { TimableOutput.println("IN-" + pr.getPersonId() + "-" + curBuilding + "-" + floor + "-" + elevatorId); }
1.3 调度器设计
- 只设计了最基础的分派器,将
Input
类中输入的请求按照楼座分给相应的楼座队列 - 交互方式为:向相应的楼座队列投入请求, 或者在输入结束时将相应的楼座队列调用
setEnd(true)
,从而等待程序结束
1.4 BUG分析
- 第一次作业出现的BUG来自于输出安全问题, 对于
TimeableOutput
没有加锁. - hack策略:
- 先用自我debug过程中发现的错误数据进行hack, 再随机生成数据
- 发现别人BUG有:
- 输出安全问题
- 电梯满载但主请求未进入电梯导致的卡死问题
2. 第二次作业
2.1 需求分析:
需求:
- 在作业1的基础之上,需要动态增加纵向电梯与横向电梯.
- 乘客允许同座不同层或同层不同座运输. 保证横向请求对应楼层有横向电梯
整体设计:
- 同楼层/同楼座的电梯均共享一个请求队列,共享同一个请求队列的电梯之间自由竞争
2.2 同步块设计
- 由于调度策略模仿给出的
ALS
策略, 导致同步块设计非常凌乱且丑陋, 而且性能完全不如采取look
策略的同学. - 自由竞争导致的额外加锁如下:
//对所有同层/座的共享队列进行加锁, judgeIn()和PersonIn()两个相似的方法均对共享队列进行加锁, 造成了逻辑上的冗余
public boolean judgeIn() {
synchronized (waitList) {
int n = waitList.getLength();
if (num < maxNum) {
for (int i = n; i >= 0; i--) {
MoveRequest pr = waitList.get(i);
if (pr.getFromBuilding() == curBuilding) {
if (mainPr == null) {
return true;
} else {
//捎带请求
if (comp(pr.getToBuilding(),
pr.getFromBuilding()) == direction) {
return true;
} else if (direction == 0) {
return true;
}
}
}
}
}
}
return false;
}
2.3 调度器设计
- 依然只设置了简单的分派功能, 根据请求的初始位置以及方向(横/竖)将其分派到不同的队列中
- 自由竞争使得调度器设计很简洁
2.4 BUG分析
-
本次作业在互测中被Hack到了一个
CTLE
的BUG, 主要原因有两点:- 无意义的
notifyAll()
太多 - 同步块加锁有太多冗余
- 无意义的
-
本次作业并没有发现其他人的BUG, 直接开摆了
3. 第三次作业
3.1 需求分析
需求:
- 本次作业中加入了不同座不同层的请求, 同时一楼初始有一部万能横向电梯来保证请求的可达性
- 横向电梯可停靠楼层可自定义, 所有电梯的基础属性均可自定义
整体设计:
- 本次作业重构了调度策略, 采取了
Look
策略+自由竞争, 优化并精简了同步控制块
3.2 同步块设计
- 经过重构, 同步块
synchronizd
只出现在:- 三种不同的请求队列的方法中
- 调度器对换乘队列加锁, 判断其是否为空, 用于实现
wait-notify
式的控制线程停止 - 本身的线程安全类
Vecotor
//本方法直接将从请求队列中得到多个请求加入到电梯的buffer, 在电梯开门之后,再将buffer的内容加入电梯内部
public synchronized ArrayList<Passenger> getReqs(int curFloor, int direction,
int curSize, int maxSize) {
ArrayList<Passenger> ans = new ArrayList<>();
int count = curSize;
int tempDir = direction;
for (int i = floorList.get(curFloor - 1).size() - 1; i >= 0; i--) {
Passenger m = floorList.get(curFloor - 1).get(i);
if (count == maxSize) {
break;
}
if (tempDir == 0) {
tempDir = comp(m.getToFloor(), m.getFromFloor());
}
if (comp(m.getToFloor(), m.getFromFloor()) == tempDir) {
count++;
floorList.get(curFloor - 1).remove(i);
passengers.remove(m);
length--;
ans.add(m);
}
}
return ans;
}
3.3 调度器设计
- 对于需要横向运输的请求,调度器会遍历寻找权重最大的横向可达楼层, 横向最多换乘一次可达. 设计的权重函数综合考虑了在该层换乘的最短路径,该层横向侯乘队列当前的长度, 并且如果该层有全楼座可达电梯,会适当降低权重, 从而充分利用电梯.
- 对于线程安全停止的问题,选择加入了一个全局的换乘队列, 如果换成队列不为空,调度器会
wait()
,换成队列在remove
一个请求的时候会notifyAll()
- 由于没有使用最短路径算法, 寻找最大权重的路径采取的方式简单, 因此采取了动态拆分请求的策略, 电梯每移动一层, 其中的换乘请求会更新当前状态并判断自己是否需要在当前层提前换乘.(因为中途可能新加入横向电梯)
3.4 架构图分析
UML图:
- 横向电梯及其横向队列与纵向电梯及其纵向队列基本一致。其中的
RequestQueue
效果等同于Vector
时序图:
3.5 BUG分析
- 本次作业并没有出现bug,自主测试采取随机生成数据的方式。
- 发现其他人的bug有:线程无法安全结束,换乘的线程安全问题导致人员无法送到指定楼层等等
4. 测试方法
- 测试主要采取随机生成数据的方式,通过
subprocess
改写程序用以记录程序的cpu使用时间。 - 对于线程安全问题,采取短时间内投放大量起点相同的请求,用以测试电梯之间的线程安全问题。
- 与第一单元测试策略的差异之处主要在于时间和正确性的检验。第二次作业如果用单线程的评测机测试会很慢,而且没有简单的直接检测正确性的方法,需要投入额外的工作量。
5.心得体会
- 线程安全问题需要高强度数据的测试才能发现,没事的时候评测机可以多挂一会(
- 每一个锁都要加的有理有据,不然可能发生死锁(虽然我并没有发现死锁的bug)。如果少调用(遗忘)了一个含有notifyAll()的方法,会导致循环等待。
- 层次化设计:感觉这一次的设计再静态角度没有什么层次性,主要就是不同线程之间的协作,在运行时有逻辑上的层次。