OO_Unit2_blog
作者:互联网
第二单元博客
1 同步块的设置和锁的选择
1.1 锁的选择
第二单元第一次课讲了synchronized
上锁的方法,而之后的课程中又讲了ReentrantLock
高级锁。尽管ReentrantLock
可以实现更多的线程控制功能,但是考虑到相对来说使用synchronized
代码实现比较容易并且不容易出错,因此三次迭代开发中均只使用了synchronized
方法给共享对象中的方法加锁。
1.2 同步块的设置
在三次作业中,我均设置了请求队列类PersonQueue
。而在第五次作业中,这个类是所有类中唯一共享的类,因此在第五次作业初期对synchronized
理解不到位的情况下,一股脑对PersonQueue
类中所有的方法前都加上了synchronized
,之后两次作业也沿用了这个传统。
而在后两次的作业中,我将PersonQueue
的封装成了VerticalTrack
和HorizonTrack
两个类,类中存放PersonQueue
数组用于电梯轨道的管理。因此在新增的两个轨道类中我也无脑地对其中的所有方法进行了synchronized
处理。
除此之外,有时电梯会在同一个条件判断语句中调用多个共享共享的方法,为保持语句块前后共享对象的状态一致,还需对电梯线程run()
方法内的语句进行同步处理。
如下面这一段在电梯线程run()
方法开头的代码,如果没有进行synchronized
同步,则在一个if()
括号内部调用verticalTrack.isEmpty()
和verticalTrack.isWaitEmpty()
时,共享对象verticalTrack
的状态可能发生变化,将会导致逻辑表达式的结果出现意想不到的状况。此外,为了保证两个if()
语句块内的verticalTrack.isEmpty()
返回值相同,我将两个条件判断语句放置到了同一个同步语句块中。
synchronized (verticalTrack) {
//process end
if (verticalTrack.isEmpty() && verticalTrack.isWaitEmpty() &&
inElevator.isEmpty() && NewMainBuilding.getInstance().isEnd()) {
return;
}
//wait for next request
if (verticalTrack.isEmpty() && inElevator.isEmpty()) {
try {
verticalTrack.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
continue;
}
}
2 调度器与线程交互方式
在三次作业中,我其实没有实现真正意义上的调度器。最多只是实现了按照出发的楼层、楼座,将电梯请求分配到了具体的队列中,而各种电梯的接送行为并没有一个统一的类去组织。
在第五次作业中,由于不涉及电梯线程之间的交互和影响,因此我没有采用调度器的方式。而在第六、七次作业中,尽管引入了多部电梯共享楼座和横向电梯的概念,但是了解往年博客后发现使用调度器和电梯自由竞争相比性能差异并不大,并且实现电梯自由竞争从难度上明显小于实现调度器,因此我在后续两次作业中仍然采用电梯自由竞争的策略。
而对于线程之间的交互,我在三次作业中均采用了生产者-消费者模式,电梯间的交互通过读写共享对象(此处为请求队列)实现。输入线程作为生产者将请求放入请求队列(托盘)中,而电梯线程作为唯一消费者每次从托盘中取走相应的请求,进行开关门上下客的处理。而在第七次作业中,由于引入了换乘,一部分的电梯请求也作为生产者,将换乘后的电梯请求添加到相应请求队列中。
3 架构模式分析
3.1 第五次作业
类的调用关系图如下。第五次作业中设置了三个线程类InputThread
,Dispatcher
,Elevator
实现电梯请求的分配和处理,它们均在主函数类中创建并运行。而共享队列PersonQueue
也在主函数中被创建,并且提供线程之间交互的渠道。
此外我还专门设置了线程安全的输出类Wrapper
负责输出,防止输出序列不递增的现象发生。
而在电梯接送策略方面,我使用了生活中绝大多数电梯使用的look
策略。但是由于我在这次作业中将每一座的所有请求放入同一个队列中,因此每次寻找最高楼层和最低楼层的请求时需要同步遍历整个队列,而且有时还要从队列中取出特定方向特定出发楼层的请求,需要传入很多参数,导致代码块设置十分混乱。因此在第六次作业中我对请求队列设置的逻辑进行了重构,对每个请求进行了精细化的分配,解决代码风格杂乱、容易出错的问题(详见下一次作业的分析)。
UML协作图如下。其实这次作业中的协作图是三次作业中最冗杂的,我设置了三级流水的线程InputThread
,Dispatcher
,Elevator
分别负责电梯请求的输入、分派、处理。但是后来发现其实电梯的输入和分派完全可以放置到同一个类中进行处理,而标志输入是否结束的isEnd
信号其实可以全局共用同一个。因此第六次作业我只设置了InputThread
和Elevator
两个线程类,更清晰简洁地处理了电梯分配的过程。
第五次我的代码架构真的很shi,出了不少bug,因此我在第六次作业时下定决心对我写的shi山进行大换shi。
3.2 第六次作业
第六次作业我将线程类简化成了InputThread
和Elevator
,并且使用了单例模式创建了NewMainBuilding
类,集中管理整个新主楼的电梯等待队列。
初始电梯线程在NewMainBuilding
类初始化时创建,而新加入的电梯线程则在输入线程InputThread
创建。此外将输出线程安全类改名成了Printer
。
吸取上一次作业的教训,我为特定楼座、特定楼层、特定方向的请求单独设置队列,将其精细化处理。具体来说,我在单例类中设置了轨道类VerticalTrack/HorizonTrack
数组,大小分别为5/10
,表示特定楼座/楼层的轨道;而每个轨道类内又设置了请求队列PersonQueue
的数组,大小分别为10/5
,表示给定楼座/楼层下从某一楼层/楼座 出发的请求队列;而每个PersonQueue
又由两个ArrayList<PersonRequest>
的列表组成,分别储存从同一地点出发,但是方向不同的两种乘梯请求。
这样一来,只需在NewMainBuilding -> Track -> PersonQueue
逐级实现getRequest
和addRequest
方法。则可以从InputThread
调用NewMainBuilding
中addRequest
方法实现请求的精细化分配,而且在获得请求时,这次作业将从队列中查找请求的过程转化为了通过出发信息检索队列的过程 ,除去了许多代码冗长的方法,提升了面向对象编程的风格特性。
关于横向电梯,我采用了类似环形的look
方法,定义了换向逻辑如下:
电梯换向条件(大前提,且关系):1.电梯是空的(注意是下客后为空)。2.上电梯的乘客中没有捎带者。3.当前轨道有请求。
此外再满足接下来两者中的一者(或关系),则电梯更换换方向:1.当前楼层有逆向的请求。2.原方向上没有请求。
至于时序图,由于删除了Dispatcher
类,因此线程的时间关系较上一次作业更为清晰。
这次作业中在只在单例类NewMainBuilidng
中设置了输入结束标记isEnd
,当输入完毕后在将其设置为true
,并且唤醒所有的电梯线程。电梯线程检测到当且的轨道为空、电梯内乘客为空并且输入结束标记isEnd
为真后,结束run()
方法。
3.3 第七次作业
第七次作业主要有三个任务。
第一个是自定义电梯参数,这个只需在电梯类内增加相应字段,简单替换方法中的参数即可。
第二个是实现电梯的换乘,本次作业中我采用了链表的方法,将官方包的类PersonRequest
封装至自定义类MyRequest
中,并且在其中设置字段nextRequest
链接到下一乘梯请求。在输入乘梯请求时,在InputThread
中根据标准换乘策略,将请求拆分成1~3个同座或同楼层的可达请求,并且将其通过字段nextRequest
链接起来。
而在PersonQueue
中,新增了一个名为waitRequests
的ArrayList
用于存放前序请求尚未执行完的电梯请求。在前序请求完成后,只需将后序请求从其对应PersonQueue
中的waitRequests
移动到同一个类中待处理的请求列表即可实现换乘。
第三个是开门权限位switchInfo
的设置与使用,其实这里只需要对水平轨道类中getRequest
方法进行修改。在获取乘梯请求时将switchInfo
信息传入,在遍历数组时将无法开门的楼座通过continue
语句跳过即可。
此外这里还有一个问题,就是对于不同的switchInfo
,相同起点相同方向的乘梯请求可能会有不一样的上梯行为。如对于在AB座又开门权限的电梯,请求A->B
能上电梯但是请求A->C
不能上,但是按照之前的分配逻辑这两个请求会放在同一个ArrayList
中,因此直接在队列中使用getOneRequest
的模式会出锅。
为解决这个问题,我首先想到了在队列中写遍历方法,但感觉这样代码会很杂乱,可能还会涉及到垂直电梯的更改,pass!最后我再次采用精细化分配的思想解决了这个问题:我在横向轨道类HorizonTrack
中将PersonQueue[5]
变为二维数组PersonQueue[5][5]
,前一个下标代表出发楼座,后一个下标代表抵达楼座,将相同起点、相同方向、但不同终点的请求放入不同的队列中,这样以来只需对HorizonTrack
中的方法调用数组的地方进行一位至二维的转化即可。
时序图和上次比较,电梯线程自身也成为了生产者。当一个乘梯请求结束并且下一个乘梯请求存在时,需要激活后续请求并且唤醒轨道上的所有电梯。
4 程序bug分析与测试策略
4.1 自己的bug及解决方法
bug可真多啊~
4.1.1 第五次作业
这次作业没做课下测试,中测过了之后就想着清明节玩去了。回来一看强测WA声一片,再仔细看看自己代(shi)码(shan),一度怀疑前几天自己的眼睛是不是瞎了。“是可AC,熟不可AC”,找到bug之后自己都给气笑了
标签:OO,请求,队列,作业,blog,电梯,线程,Unit2,bug 来源: https://www.cnblogs.com/cccvs/p/16216406.html