一、作业设计分析
第五次作业
思路:
作业要求:
- 本次作业仅要求一个电梯,基本没有任何限制
类设计(除去主类):
Dispatcher
类:继承自Thread
,本次作业调度器基本是个空壳子,直接与输入是在一起的Elevator
类:继承自Thread
,模拟电梯运作Person
类:输入的请求转成Person
类存放
线程设计:
- 我选择使用了两个线程,输入线程和电梯线程
调度策略:
- 在看了个各种电梯调度策略之后,发现好像并没有最完美的调度策略,最后选择采取了与ALS较为相近的可捎带策略,稍微做了一丁点的改动
线程安全性:
- 将
Dispathcer
类设计为线程安全类 - 考虑在对请求队列遍历过程中可能会有新请求加入,且除此之外不会做其余改动,不会出现其他安全问题,因而在遍历请求队列时未加锁
一些具体实现:
- 调度器与电梯共享同一个请求池(请求池本次放在了
Dispatcher
中,设置了相关的static方法,使得电梯可以直接访问) - 电梯在每一层主动检查请求池,电梯调用
checkIn()
和checkOut()
方法,检查是否有人要进或者出
度量分析
UML图
各个类的复杂度分析
- 分别有OCavg(类的方法平均循环复杂度)和WMC(总循环复杂度)两个指标,可见所有指标都没有飙红,复杂度尚可
总长400行左右
第六次作业
思路:
作业要求:
- 本次作业要求按照输入的整数,开启3-5台电梯,每台电梯限乘7人
类设计(除去主类):
Person
类:仍然是存放请求Pool
类:总的请求池,所有的方法和属性均为static
,对所有类可见,所有类共享Input
类:继承自Thread
,输入线程ElevatorStatus
类:存储电梯状态,电梯与调度器共享RequestPool
类:每个电梯独有的请求池,各电梯间不互通,分别与调度器共享Elevator
类:继承自Thread
,模拟电梯运作Dispatcher
类:继承自Thread
,调度器类,负责将总的请求池Pool
中的请求分配给每个电梯的RequestPool
线程设计:
- 本次作业输入线程和调度器线程是两个独立的线程,除此之外每个电梯一个线程
调度策略:
-
电梯的调度策略:基本没有修改,只是将
checkOut()
方法中的请求池换成自己独有的那个 -
调度器的调度策略:
优先将请求分入可以捎带的电梯的请求池中,前提是此电梯请求池的人数加上电梯内的人数小于7
无捎带的情况下,则优先寻找空电梯
都不满足的情况下则调用
Pool
的waitForChange()
方法等待,防止轮询,当Pool
有新请求加入和任意一个电梯的目标楼层被改变时会唤醒Dispatcher
,唤醒后遍历Pool中请求
线程安全性:
ElevatorStatus
类、Pool
类、RequestPool
类均为线程安全类
线程思路分析
度量分析
UML图
各个类的复杂度分析
- 电梯和输入类个人认为可修改性不大,调度器的方法在每次电梯状态改变时都会重新调用,导致循环复杂度偏高,还有修改空间
总长650行左右
第七次作业
思路:
作业要求:
- 本次作业要求先开启3台电梯分别为A类、B类、C类,并且可根据输入新增对应类型的电梯
- 不同类型的电梯有不同的可停靠楼层,移动速度和最大载客量
类设计(除去主类,比第二次作业新增):
ElevatorStatusFactory
类:在每个ElevatorStatus
中存储了电梯的各种信息,为了防止Input
的run方法过于冗长,建立此类,根据新加入的电梯ID和类型创建
线程设计:
- 本次作业仍是输入线程和调度器线程是两个独立的线程,除此之外每个电梯一个线程
调度策略:
-
电梯的调度策略:基本没有修改,只是到了每层之后检查是否可以停靠,如果可以停靠才调用
inAndOut()
-
调度器的调度策略:
先检查是否需要换乘,如果需要换乘对请求进行修改
其余部分基本与上次作业一样,只需要检查是否可以乘坐该电梯
线程思路分析
度量分析
时序图
UML图
各个类的复杂度分析
- 本次复杂度基本和第二次没有区别
总长800行左右
二、 可扩展性分析
- 对最后一次作业进行可扩展性分析
基于S.O.L.I.D.原则:
SRP原则(单一责任原则):
- 每个类各司其职,电梯类只负责自己电梯的调度,调度器负责将请求分配给各个电梯
OCP原则(开放封闭原则):
- 本单元未进行重构,方法间也没有混杂在一起,基本满足OCP原则
LSP原则(里氏替换原则):
- 在本单元作业中未用到继承
ISP原则(接口分离原则):
- 本单元作业未使用到接口
DIP原则(依赖倒置原则):
- 这一部分不能很好的把握,在编程过程中主要还是对具体类进行编写
三、Test与Bug
Bug
-
在最后一次作业的互测中出现了一个bug,当最后一个人下了电梯准备进行换乘时,电梯停止运行了
-
但是自己本地跑了上百遍也没能复现出来,无奈之下开始肉眼检查安全问题,最后发现一个很小几率会出现的bug
public synchronized void getOut(Person person) {//电梯的getOut方法
SafeOutput.output("OUT-" + person.getId() + "-" +
status.getFloor() + "-" + status.getElevatorId());
if (person.getNeedTrans() && !person.getHasTrans()) {//需要换乘的情况
person.transfer();
Pool.addRequest(person);
status.out(person);
} else {//不需要换乘的情况
status.out(person);
Pool.alert();
}
}
public Boolean checkOver() {//调取器的checkover方法
Boolean over = Pool.getOver() && Pool.isEmpty();
for (int i = 0; i < status.size(); i++) {
over = over && request.get(i).isEmpty() && status.get(i).isEmpty();
}
return over;
}
- 由于
checkOver()
方法的非原子性,倘若只有一台电梯剩最后一个需要换乘的乘客,此时如果checkOver()
方法先运行Pool.isEmpty()
方法,之后运行了电梯getOut()
方法,然后调度器再运行for
循环的话,就会误判断此时请求队列和电梯均没有人,而判断应该结束。 - 针对此bug,由于很难通过加锁的方式将该操作原子化,最终我选择通过更改
checkOver()
的执行顺序,将Pool.isEmpty()
放在最后执行,利用逻辑将其避免
Test
由于线程安全问题不一定能稳定复现,我的主要测试是测试WA顺带测试线程安全的问题:
-
通过大量的自动生成数据集自动化定点投放输入测试+判定结果正确性
-
当发生错误时分析代码逻辑,定位错误原因,再测试出错的一批数据集
但是自己的线程安全问题没能在课下测出来TAT
四、心得体会
-
跟单线程相比,多线程更考验我们的设计以及我们程序设计的严谨性和周密性。
-
但是也不可为了安全滥用锁,一是锁太多很繁重,对锁的滥用也很容易产生一些死锁等安全问题。
-
多线程因为不可复现、不可调试的特性,让大家的debug之旅更加痛苦了,尽管我在编程的过程中已经万分谨慎,但是线程安全问题他还是来了。
-
多线程为了节省CPU的时间一定要用好wait和notify。