BUAA-OO-第二单元总结
架构设计分析
hw5
uml类图
架构设计
在本单元第一次作业发布之后,我先是浏览了学长的博客,分析了学长的架构。与往年第一次作业要求相比,今年的第一次作业保证了输入乘客请求时,已经为乘客分配好了电梯,即确定了乘客将要乘坐的电梯,因此我认为本次作业不需要Schedule
类,于是大胆了省略了Schedule
类,认为在后续迭代中也可以再加入。我在本次作业中主要应用了生产者-消费者模式进行架构设计。
我在第一次作业一共设置了7个类,分别是Mainclass
、Advice
、Elevator
、InputThread
、Person
、RequestTable
、Strategy
。
Mainclass
是主线程,负责启动InputThread
线程。
InputThread
是输入线程,负责处理输入、启动Elevator
线程,根据输入中的信息new
出一个Person
对象并加入对应的Elevator
含有的RequestTable
中。
Elevator
是电梯线程,负责处理RequestTable
中的请求。根据内置Strategy
类发出的运行建议进行运行,将每位乘客运送至适当的位置。
Strategy
是策略类,含有对应电梯的RequestTable
,根据RequestTable
和电梯此时的状态提供建议,我主要采取了LOOK算法进行运行策略的给出。
Person
是乘客类,含有一个乘客的id信息、出发楼层、目的楼层等信息。
RequestTable
是请求池类,含有该部电梯待处理的所有请求,可以支持输入线程加入请求,也可以支持电梯线程完成请求后删除请求。
Advice
类是枚举类,含有电梯开关门、移动、等待、转向、结束等动作,可增加代码可读性。
调度器
如前言,我在本次作业中没有设置调度器,每个请求已含有即将乘坐的电梯的信息,因此我在InputThread
类中每得到一个输入,转化为Person
对象后就加入了对应电梯的RequestTable
中,实现了调度。
同步块与锁
由于是第一次接触多线程编程,在最开始的时候不是很理解什么是锁,什么是同步块,以及不设置锁为什么会导致线程安全问题。在跑了跑公众号的示例代码,简单学习了关于sychronized
关键字的文章后,我略微有了一点眉目。分析本次作业,可以发现可能会产生线程安全问题的只有共享对象RequestTable
,InputThread
会对RequestTable
进行写操作,而Elevator
会对RequestTable
进行读和写,如果不加锁则会导致线程安全问题。虽然通过各种渠道收集到的信息都说同步块是更好的选择,因为可以灵活设置同步块内的代码内容,减少不必要的加锁,提升程序运行效率,但同步块对于新手难度较大,可能会遗漏某些应加入同步块的代码。因此我最终采用了对方法加锁的对象,我对RequestTable
类中的所有方法都添加了sychronized
关键字,来解决线程安全问题。
public class RequestTable {
//code
public synchronized void setEndFlag();
public HashMap<Integer, ArrayList<Person>> getRequestMap();
public synchronized void addRequest(Person person);
public synchronized boolean isEnd();
public synchronized boolean isEmpty();
public synchronized void delRequest();
//code
}
hw6
uml类图
架构设计
第二次作业相比第一次作业,新增了reset指令,电梯在接受到reset指令后需要尽快停靠在某层(最多输出两条ARRIVE),然后将电梯内的所有乘客放在该层,进入reset状态,reset状态结束后该电梯的限载人数和运行速度可能会发生变化。其次每个乘客请求不限制前去接他的电梯,即可采取任意的调度策略。
为满足第二次作业的需求,相比上一次作业我新增了两个类,分别是ResetTable
和Schedule
,因其他类的功能并未发生显著变化,因此在次仅介绍新增的两个类。
ResetTable
是reset请求池类,含有该部电梯待处理的所有reset指令,可以支持输入线程加入reset请求,也可以支持电梯线程在结束reset状态后删除reset请求。
Schedule
是调度器类,负责将总请求池mainRequestTable
中的请求按某种策略分配给各部电梯,在此简要介绍一下我了解到的各种调度策略。
首先就是大名鼎鼎的随机分配和模6分配了,这两种调度策略极易实现,且性能也不算太差,每种情况都比较平均。
其次是影子电梯,深克隆6部电梯,模拟执行完所有当前请求所需的时间,将请求分配给用时最少的电梯。
最后是调参法,即综合考虑运行时间、限载人数、运行速度的指标,分配给得分最高的电梯。
调度器
很可惜的是,在写第二次作业的时候我没有完全弄懂影子电梯,调参法也不是很确定参数的设定,也不想直接用随机分配和模6分配(现在十分后悔),于是自己摸索了一种调度方法,当时觉得效率还不错,但因为一点疏忽在强测中并没有得到很好的性能分,也在互测中被卡rtle。我最终采用了如下的调度策略:总是将请求分配给当前不处在reset状态且没有超载且根据当前状态计算出去接这个乘客用时最少的电梯(阉割版影子电梯)。
同步块与锁
首先RequestTable
类与第一次作业一样,我对其中所有的方法都加了锁,在本次作业新增的ResetTable
类里,我也对所有的方法加了锁。
public class ResetTable{
//code
public synchronized void addReset(ResetRequest resetRequest);
public synchronized boolean isEmpty();
public synchronized ArrayList<ResetRequest> getResetRequests();
public synchronized void delReset();
//code
}
其次由于新增了Schedule
类,原先电梯线程的退出条件发生了一定改变,我采用了如下方法实现电梯线程的结束:
if (mainrequestTable.isEmpty() && mainrequestTable.isEndFlag()) {
for (int i = 0; i < eleRequestTable.size(); i++{
eleRequestTable.get(i).setEndFlag();
}
return;
}
即总池空了且总池得到了结束标志(由输入线程设置),就可以设置电梯的结束标志了。
hw7
uml类图
架构设计
第三次作业相比第二次作业,增加了双轿厢电梯。输入中增加了双轿厢reset指令,与第二次作业中的reset的不同之处在于电梯结束新型reset状态后,由单轿厢改变为双轿厢,并设置好了恰当的换乘楼层。从此之后该电梯轨道内拥有2个轿厢,分别为A、B。A轿厢只能在1至换乘层间移动,B轿厢只能在换乘层至11层间移动,且换乘层在任何时刻只能有一个轿厢(一个轿厢进入时需要确保另一个轿厢不在换乘层,即避免碰撞)。
相比第二次作业,我增加了如下类:DoubleCarElevator
、DoubleCarStrategy
、DoubleCarResetTable
、ElevatorController
。
DoubleCarElevator
是双轿厢电梯类,本质与Elevator
类没什么区别,只是我懒得再写一个总电梯类然后让单轿厢电梯和双轿厢电梯继承这个类了。
DoubleCarStrategy
是双轿厢策略类,由于双轿厢电梯的运行区域受到换乘层的限制,且轿厢进入换乘层后,需要将所有乘客放在这一层,并通知伙伴轿厢前来接走这些乘客,实现换乘需求,因此双轿厢电梯的策略类需要进行一定的修改。我大体上还是采用了LOOK算法,只不过在细节上进行了微调,例如修改了运行范围,开门限制。
DoubleCarResetTable
是新增reset请求池类,含有该部电梯待处理的所有新增reset指令,可以支持输入线程加入新增reset请求,也可以支持电梯线程在结束reset状态后删除新增reset请求。
ElevatorController
是公共枢纽类,存放了双轿厢电梯的伙伴的相关信息。由于需要避免换乘层碰撞问题,所以我设置了这个类帮助双轿厢电梯获取伙伴的相关信息。
调度器
经历了上一次作业调度策略的失败,再加上本次作业复杂度的提升,我深感无力进行调度方面的优化,因此我转向了模6分配,性能方面较为平均。
同步块与锁
首先我依旧是对RequestTable
、ResetTable
、DoubleCarResetTable
中的所有方法加了锁,其次对本次架构进行分析发现,ElevatorController
类也是一个共享对象,因此我对其中的方法也加了锁。
public class ElevatorController {
public synchronized void setElevator(Elevator elevator);
public synchronized HashMap<Integer, ArrayList<Person>> getADestMap();
public synchronized HashMap<Integer, ArrayList<Person>> getBDestMap();
//....
}
这只是能够通过中测的版本,未解决所有的线程安全问题,后续debug时对加锁和同步块的修改我将在bug分析中进行补充说明。
避免换乘层碰撞
我通过如下方法避免了换乘层碰撞问题:
if (direction == 1) {
if (type.equals("A")) {
try {
if (nowFloor == (exchangeFloor - 1)) {
while (elevatorController.getNoticeB() &&
!elevatorController.getIsWaitB() &&
!elevatorController.getIsOverB()) {
Thread.sleep(100);
}
elevatorController.setMoveA(true);
} else {
elevatorController.setMoveA(false);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
double sleep = moveSpeed * 1000;
Thread.sleep((long) sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
elevatorController.setMoveA(false);
nowFloor++;
TimableOutput.println("ARRIVE-" + nowFloor + "-" + id + "-" + type);
} else {
if (type.equals("B")) {
try {
if (nowFloor == (exchangeFloor + 1)) {
while (elevatorController.getNoticeA() || elevatorController.getIsMoveA()) {
Thread.sleep(100);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
double sleep = moveSpeed * 1000;
Thread.sleep((long) sleep);
} catch (InterruptedException e) {
e.printStackTrace();
}
nowFloor--;
}
核心在于状态分析,对A轿厢而言,当它处在换乘层下一层且需要移动至换乘层时则进行判断,如果此时B轿厢位于换乘层或换乘层上一层且它不处于wait状态也不处于结束状态,则强制休眠A轿厢,否则A轿厢进入换乘层;对于B轿厢而言,当它处在换乘层上一层且需要移动至换乘层时则进行判断,如果此时A轿厢位于换乘层或正在往换乘层移动,则强制休眠B轿厢,否则B轿厢进入换乘层。可以看出我设置B轿厢优先级略高于A轿厢,通过这个逻辑避免了换乘层碰撞问题。
最终uml协作图
bug分析
hw5
本单元第一次作业难度并不算高,在借鉴了学长架构和实验代码之后,我算是比较快的完成了本次作业,在周围同学评测机的轮番轰炸下,并没有发现什么bug。最终强测和互测均没有出现bug,只不过损失了一点性能分。
hw6
在本次作业中我的bug主要集中在ctle方面,即线程产生了轮询。这种bug还是非常好de的,通过printf
大法就可以很快获知是哪个线程一直在轮询,经过检查我发现我的Schedule
线程产生了轮询,原因是结束条件那里产生了问题,我最开始的写法是如果6部电梯线程都结束了Schedule
线程才退出,后来修改为只要总池为空且总池结束标志已到就结束,成功解决了轮询问题。
强测最终得分为92分,全部损失在了性能方面。互测中我被hack的点集中在rtle,我的调度策略出现了问题,我不会给正在reset的电梯分配请求,也基本不会给超载的电梯分配请求,因此如果当前只有1部电梯未处于reset状态或6部电梯都超载,在这种极限情况下我会将大量请求只分配给1部电梯,导致了rtle。不过这个bug也比较容易解决,我简单修改了调度策略,便修复了互测bug。
hw7
在本次作业我的bug主要集中在线程安全方面,我的程序有几率触发Java.util.ConcurrentModificationException
,经过检查最终发现原因出在在循环遍历RequestTable
和destMap
时,没有设置好同步块,导致出现了线程安全问题,我采用如下方法解决:
private boolean canOpenForIn(int nowFloor, int nowNum, int personLimit, int direction) {
if (nowNum < personLimit) {
synchronized (requestTable) {
for (Person person : requestTable.getPersonRequests()) {
//...
}
}
//...
} else {
return false;
}
}
private boolean hasReqInOriginDirection(int nowFloor, int direction) {
if (direction == 1) {
synchronized (requestTable) {
for (Person person : requestTable.getPersonRequests()) {
//...
}
}
return false;
} else {
synchronized (requestTable) {
for (Person person : requestTable.getPersonRequests()) {
//...
}
}
//...
}
}
private boolean canEnd(HashMap<Integer, ArrayList<Person>> destMap, int exchangeFloor) {
synchronized (partnerRequestTable) {
for (Person person : partnerRequestTable.getPersonRequests()) {
//...
}
}
synchronized (destMap) {
for (int i = 1; i <= 11; i++) {
if (destMap.containsKey(i)) {
for (Person person : destMap.get(i)) {
//...
}
}
}
}
return true;
}
即通过对RequestTable
和destMap
添加sychronized
关键字,构建同步块来解决线程安全问题。
心得体会
- 多线程bug真的非常难找,因为存在复现概率很低的情况,极大的锻炼了我的耐心和debug能力,尤其是对
printf
大法的深刻领悟。 - 基本掌握了中小型多线程程序的编写,对锁、线程安全等概念有了基本认知。
- OO进程已经过半,无论前两个单元表现如何,还是要重整行囊、继续出发,向第三单元迈进!