OO第二单元总结
OO第二单元电梯总结
架构模式
Hw5, Hw6, Hw7三次作业架构基本没有巨大变化,属于增量的叠加开发
hw5 一级生产者消费者模型with策略类分离
第一次作业, 我做了两种架构的尝试, 写了:
-
调度线程祭天型 单托盘
-
带调度器线程的两级托盘
在尝试写了两种架构的基础上, 我分析了一下两种架构,
-
不对任何数据进行区分, 所有数据对电梯可见
-
是二级生产者消费者模型, 仅仅对每个楼进行划分,每栋楼之间的请求隔离, 楼内请求电梯间是互通的
在我的两种架构里, 第二种架构的scheduler
线程仅仅负责取总托盘的请求,然后按楼区分发。本质上和你电梯直接去查找大托盘, 各取所需, 在功能上似乎没有任何区别。 反而调度祭天,减少了一个线程, 首先会简单不少, 其次, 每个电梯可见所有请求, 或许扩展性反而更好, 所以最终我选择了1作为三次作业的基本架构。
总之,对于架构的选择取舍就是:
要获取高效简洁解决问题的架构, 就得在调度自由度和懒癌(bushi, 简单度之间做取舍
————沃兹基朔德
关于策略类的提取:
体现了DIP依赖倒置原则高级模块不应该依赖低级模块,而是依赖抽象接口,通过抽象接口使用对应的低级模块
关于优化实现了:
-
量子电梯, 通过记录上次arrive & close 时间, 减少了wait以后,再被唤醒,可以快速的度过本层(其实就是拿一部分沉睡时间当作运动中的时间)比如:
private void moveArrive() { try { Thread.sleep(max(400 + (lastArriveTime - System.currentTimeMillis()), 0)); // Thread.sleep(400); } catch (InterruptedException e) { e.printStackTrace(); } building = (building + dir + 5) % 5; // arrive MyOutput.println("ARRIVE-" + buildingNameList[building] + "-" + floor + "-" + elevatorId); }
-
开关门400ms恒定保持
-
先下后上,尽量哆啦人(这不是共识吗?但我和其他童鞋交流时发现他没有,姑且算是吧
hw6 增加了两个电梯子类的一级生产者消费者模型
这次作业中, 可以看到最令人神奇的就是:
- 加入了横向电梯
- 动态新增电梯
- 不考虑换乘
这就意味着我们要为环形电梯设计新的的调度策略, 同时多个电梯这一次终于可以互相进行抢人, 这就带来了竞争和分配如何选择的问题
-
对于新增横竖电梯,还存在行为上的差异, 所以我搞了两个子类, 分别处理横竖方向的电梯
-
动态新增电梯, 这里其实暗示我们搞线程池, 然鹅我也现在才发现, 如果以后有机会可以搞个线程池, 其次由于选择了调度线程寄了天, 自然选择
-
提前为换乘打余量,我将托盘中的
PersonRequest
改成了ConcurrentLinkedQueue<PersonRequest>
链表,具体怎么换乘hw7来说 -
环形电梯调度策略:
采用了类ALS基准策略, 效果还可以, 基本性能分都拿到了, 98+不戳的
Look也写了,但是担心出bug, 所以没有采取这个策略对象, 用了ALS, 当然Look肯定写好了是更快的
hw7 带换乘的一级生产者消费者模型
可以看到基本没改什么, 就是在Input类中加入了一个换乘拆分函数
线程的协作简图
主要就是
- 输入线程,接收到输出, 进行请求拆分, 或者创建电梯线程
- 电梯线程, 在while循环中, 每次检测是否wait或者return, 此后决定上下人,并依据上下人开关门, 最后决定方向并前进
同步块和锁
总结分析三次作业中同步块的设置和锁的选择,并分析锁与同步快中处理语句之间的关系
三次作业架构基本保持了一致, 且同步块基本也保持了一致, 取最后一次作业的同步块讲解:
首先是 RequestTable.java中
// InputHandler 添加请求, Elevator送人到中转站时
public synchronized void addRequest(ConcurrentLinkedQueue<PersonRequest> a) {
requests.add(a);
}
// personLeft维护的是 没有送达目的地的人数
// InputHandler接收到请求时
public synchronized void addPersonLeft() {
this.notifyAll();
personLeft++;
}
// Elevator送人到目的地
public synchronized void subPersonLeft() {
personLeft--;
}
// 输入结束
public synchronized boolean isInputEnd() {
return inputEnd;
}
// 输入结束
public synchronized void setInputEnd(boolean inputEnd) {
this.notifyAll();
this.inputEnd = inputEnd;
}
// 上下是否没有请求了
public synchronized boolean isUpDownEmpty(String buildingName) {
...
}
// 同理
public synchronized boolean isLeftRightEmpty(int floor) {
...
}
// 前方是否有请求
public synchronized boolean upDownRequestAhead(String buildingName, int floor, int velocity) {
...
}
// 人都送到了
public synchronized boolean noPersonLeft() {
return personLeft == 0;
}
Strategy类木有锁, 不互斥的给出运动方向、上下人
public interface Strategy {
public Vector<PersonRequest> getPickUpRequests(Vector<ConcurrentLinkedQueue<PersonRequest> inside,
int floor, int dir, int capLeft);
public boolean turnAround(Vector<ConcurrentLinkedQueue<PersonRequest> inside, int floor, int dir, int capacity);
}
InputHandler中的锁:
// 用于判停,判wait
private boolean checkWaitReturn() {
synchronized (requestTable) {
while (this.isempty() && requestTable.isLeftRightEmpty(floor)) {
if (requestTable.isInputEnd() &&
requestTable.isLeftRightEmpty(floor) && requestTable.noPersonLeft()
&& this.isempty()) {
requestTable.notifyAll();
return true;
}
try {
requestTable.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
return false;
}
主要就是锁住了RequestTable 防止 check & modify, read & write, write&write 的线程危险, 使得其互斥。
还有就是电梯在运行时需要查看一下情况,其实也可以放在RequestTable类里。
调度器设计
hw5的一个版本里, 我的调度器主要就是负责按楼层分发, 没有任何特殊性,感觉名存实亡就是摆设, 所以就舍弃了调度器线程。
我的调度器线程没了, 调度主要就是自由竞争。
同时换乘请求拆分时, 优化了一部分基准策略, 按概率随机分配拆分楼层, 使得拆分楼层分布保持较为均匀
bug与测试 总结
强测&互测bug
三次作业总共出现了一个bug, 就是我第一次作业最后提交的时候一着急, 不小心把上下人逻辑搞错了, 导致可以承载的人数过多,wa了好几个点,真的心痛, 血的教训。
debug最重要的是按部就班的按debug流程来, 不要着急,不能东一榔头西一棒槌,都可能错过你的bug.
ps: 中测真的很弱, 感觉就是把你骗进来杀
————沃兹基朔德
比如在那以后我就规定了这么一套流程,防止我心急或者漏了什么,导致bug被miss过去, 看着图一乐吧,毕竟我真是太粗了, 这些流程守好了, 别丢东西了
遇到的记忆深刻的bug
本地测试与测评机
测评机三次作业都做了, 还和小伙伴一块分享一块测试, 真不戳!评测机也是不断迭代hhh
从最开始只能测一个人,到支持多个人一起pk速度
唯一不足就是对于超时应该及时掐断, 我还没来及整, python多线程有时间学一波吧
心得体会
线程安全心得
线程安全的心得在于,关键在于设计线程安全类, 通过线程安全类来保证各个线程的行为是安全的, 比如通过锁的方法保证互斥, 通过资源也同时完成了线程间的协作通信。 关键在于选取合适的临界区, 保证线程安全的情况下尽量小。
层次化设计&设计原则
三次作业基本是迭代的, 每次作业都在上次的作业上或是继承, 或是新增了一些功能, 较好的实现了代码复用
SRP: 职责应该单一,不要承担过多的职责。体现在每个类只负责自己的行为, 比如:
电梯只负责开关门, 上下人, 停止, 转向, 前进。
RequestTable仅仅负责,根据电梯的信息进行搜索并返回数据,以及数据的更删查改。
InputHandler仅仅负责拿到请求、处理请求
OCP:修改软件功能的时候,使得他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。 可以从三次代码的结构是迭代的, 仅仅新增了部分代码, 实现代码复用。
ISP: 接口的内容一定要尽可能地小,能有多小就多小。 我将策略类的接口仅设置了2个方法,转向、上下人,最小限度的保持了策略类对电梯的控制
LSP, DIP: 体现在策略类的分离体现了 ,前面说过了,就不重复了
真情实感
做首诗纪念一下
电梯月
——cywuuuu
欢送电梯月,
可惜有bug。
下次再努力,
争取bugfree!