OO第二单元作业总结
OO第二单元作业总结
概述
本单元共三次电梯系统设计作业,难度递增,架构可以层层嵌套(每次都设计好的话),趣味颇丰。电梯的款式(奇怪的描述)均为目的选层电梯,即乘客可在电梯外发送自己想要去的楼层。
乘客的请求包括出发楼层和目的楼层与id,每位乘客都有自己独有的id,一个id只能对应一个请求,本单元不需考虑WF检测。
一、设计策略
1.1第一次作业总结
第一次作业电梯名为单部多线程傻瓜调度(FAFS)电梯。傻瓜调度就明确指出不需要关注电梯性能,只需要完成接送人的任务,因此我菜用了较为直观的设计思路:
电梯属性包括:1个请求队列(属于本电梯还在电梯外某楼层等着的人)、1个电梯内部队列(电梯内部的人)、电梯现在的楼层。
类设计包括:Main(轮询接受输入)、Elevator(执行单元)、Waiter(请求类)、Inner(电梯内请求类)。共4个类
运行逻辑如下:
1、Main函数轮询(输入接口等待输入,无输入时阻塞不占用CPU)接受输入的请求,将请求发送给电梯。
2、若输入NULL,则由Main函数向电梯发送停止信号。
3、电梯轮询依次判断以下内容:
(1)、若电梯内部队列不为空,则执行电梯内乘客请求(将乘客送至指定楼层)
(2)、若请求队列内若有请求,则执行该请求(电梯先去接乘客,然后将乘客送至指定楼层)
(3)、轮询结束条件:请求队列为空 && 电梯内部队列为空 && 接收到停止信号
总结:本次作业未使用调度器(确实一开始没想到有这种高级策略),但因为本次作业较为简单,并未受到影响。了解到了ArrayList线程不安全,以及简单的多线程运行逻辑,为第二次作业铺设了基础。
1.2第二次作业总结
本次作业电梯名为单部多线程可捎带调度(ALS)电梯。可捎带是本次作业核心,如何完成“捎带”成为本次作业的难点。设计架构沿用第一次傻瓜电梯的架构(难得的没有重构),设计思路如下:
电梯属性包括:1个请求队列(属于本电梯还在电梯外某楼层等着的人)、1个电梯内部队列(电梯内部的人)、电梯现在的楼层。(完全沿用第一次的电梯架构)
类设计包括:Main(轮询接受输入)、Elevator(执行单元)、Waiter(请求类)、Inner(电梯内请求类)。共4个类(完全沿用第一次的电梯架构)
电梯执行逻辑:电梯只执行主请求(电梯内最早进来的那个人),即电梯内部队列里的第一个元素。
接人逻辑(捎带判断):接人的条件需满足以下条件:(1)、捎带请求与电梯主请求同方向。(2)、捎带请求的出发楼层位于电梯主请求出发楼层与目的楼层之间(电梯到达某一楼层后,通过对请求队列的循环判断来实现,若满足条件,则执行捎带)。
运行逻辑如下:
(电梯每到一层,对请求队列循环判断)
1、Main函数轮询(输入接口等待输入,无输入时阻塞不占用CPU)接受输入的请求,将请求发送给电梯(将接受到的请求加入电梯的请求队列),并唤醒电梯(执行notify)。
2、若输入NULL,则由Main函数向电梯发送停止信号。
3、电梯非轮询依次判断以下内容:
(1)、若电梯内部队列为空 && 请求队列为空 && 未接收到停止信号 :电梯执行wait()操作(电梯线程由执行态转换至waiting态,等待被notify)
(2)、若电梯内部队列不为空,则执行电梯内乘客请求(将乘客送至指定楼层),同时进行捎带判断。
(3)、若请求队列不为空,则执行该请求(电梯先去接乘客,然后将乘客送至指定楼层),同时执行捎带判断。
(4)、电梯停止运行条件:请求队列为空 && 电梯内部队列为空 && 接收到停止信号
总结:本次电梯主要难点在于实现捎带逻辑,我的捎带逻辑也许不是最优化的(从系统总运行时间来说),但可以满足题目的要求。本次实验了解了wait()和notify()的使用方法,初步接触了 synchronized关键字的使用,利用先有请求后执行这一逻辑避开了“电梯wait但无人唤醒”的情况,本次作业做完之后才了解到“调度器”这一概念,虽然没在这次作业中体现,但是提前完成了调度器的设计,为第三次终极智能电梯打好了基础。
这次实验由于我的疏忽大意(arrive输出时间疏忽),强侧分数有点恶心,但是总体上玩的还是很开心。
1.3第三次作业总结
本次作业电梯名为多部多线程智能(SS)调度电梯。“多部”、“智能(电梯已经是成熟电梯了,会自己分辨请求了)”是本次作业核心。电梯的设计架构沿用第二次捎带电梯的架构(难得的再次没有重构),同时实现了调度器线程,调度器与电梯存在上下级的关系,设计架构如下:
本次电梯与前两次电梯具有电梯属性的不同,包括以下内容:电梯容量,电梯可达楼层,不同电梯速度不同(和调度策略的设计有关)。
电梯属性包括:1个请求队列(属于本电梯还在电梯外某楼层等着的人)、1个电梯内部队列(电梯内部的人)、电梯现在的楼层、电梯名、电梯容量、1个电梯可达楼层的ArrayList(方便使用contains方法)。(部分沿用第二次的电梯架构)
类设计包括:Main(轮询接受输入)、Elevator(执行单元)、Waiter(请求类)、Inner(电梯内请求类)、Dispatch(调度器)。共5个类(部分沿用第二次的电梯架构)
电梯执行逻辑:电梯只执行主请求(电梯内最早进来的那个人),即电梯内部队列里的第一个元素。
接人逻辑(捎带判断):接人的条件需满足以下条件:(1)、捎带请求与电梯主请求同方向。(2)、捎带请求的出发楼层位于电梯主请求出发楼层与目的楼层之间(电梯到达某一楼层后,通过对请求队列的循环判断来实现,若满足条件,则执行捎带)。
调度原则:Main函数发送请求给调度器线程,调度器线程将请求存入调度器内请求队列,将请求根据需要分成4个请求:1、乘客可乘A电梯直达目标楼层。2、乘客可乘B电梯直达目标楼层。3、乘客可乘C电梯直达目标楼层。4、乘客需要换乘,执行换乘逻辑。
调度器换乘逻辑:根据三部电梯的可达楼层分析,乘客在起始楼层和目标楼层之间选择一部换乘电梯并选择一个换乘楼层(以先A后B后C原则,因为A电梯最快,这个的算法并不好),然后将乘客标记为需要换乘,将请求发送给电梯。
电梯换乘逻辑:若电梯发现将要出电梯的某乘客为换乘乘客,则向调度器发送一个新请求,将换乘乘客信息重新发送给调度器(即将换乘楼层变更为起始楼层,目标楼层不变,乘客id不变),新请求将唤醒调度器,并执行调度器内换乘乘客计数器--(加锁)。
调度器使用单例模式,即私有构造并设立getinstance()方法。
Main函数运行逻辑如下:
1、Main函数轮询(输入接口等待输入,无输入时阻塞不占用CPU)接受输入的请求,将请求发送给调度器(将接受到的请求加入电梯的请求队列),并唤醒调度器(执行notify)。
2、若输入NULL,则由Main函数向调度器发送停止信号,并结束Main函数。
调度器对进行非轮询依次判断以下条件,Run方法运行逻辑如下:
1、若调度器请求队列为空 && (未收到Main函数发出的停止信号 || 存在换乘乘客) ,则执行wait()操作,等待被唤醒。
2、从调度器请求队列中取出一个乘客,将该乘客根据调度原则,将请求分配给三个电梯中的一个(每个电梯都有自己内部的请求队列,即该电梯能够接的人,电梯对于调度器请求队列一无所知),并从调度器请求队列中移除该请求。若发现乘客需要换乘(执行调度原则4),则将调度器内换乘乘客计数器++(加锁)。
3、若调度器请求队列为空 && 收到Main函数发出的停止信号 && 不存在换乘乘客 ,则向三部电梯发送停止信号,并停止调度器线程
电梯运行逻辑如下(沿用第二次可捎带电梯的架构):
电梯非轮询依次判断以下内容:
(1)、若电梯内部队列为空 && 请求队列为空 && 未接收到停止信号 :电梯执行wait()操作(电梯线程由执行态转换至waiting态,等待被notify)
(2)、若电梯内部队列不为空,则执行电梯内乘客请求(将乘客送至指定楼层),进行捎带判断,进行电梯换乘逻辑判断。
(3)、若请求队列不为空,则执行该请求(电梯先去接乘客,然后将乘客送至指定楼层),进行捎带判断,进行电梯换乘逻辑判断。
(4)、电梯停止运行条件:请求队列为空 && 电梯内部队列为空 && 接收到调度器发出的停止信号
设计思路:这次换乘电梯最终构想是将换乘乘客标记,等到需要换乘时,由电梯向调度器输送请求,但是这么设计是不符合实际的,应该由调度器统筹安排电梯,电梯作为执行单元,不应该影响调度器的工作,但因为个人能力有限,确实没能想到一个好办法,实现和电梯低耦合关系的调度器。
总结:本次电梯主要难点在于实现换乘,我的调度逻辑有大优化空间,但是因为个人能力有限,就没有追求性能优化(怕改不好改出一堆bug)。我的换乘方法决定了我的调度器的停止条件,这个停止条件是经历了很多次输出debug才最终确定的,确实对于多线程的理解有了很大帮助。很感谢助教团队的辛勤构思,要不然我是没有这个自信,仅仅第二次作业,就完成“不重构代码”这一重大进步。这次的方法复杂度教前两次有了一个明显提升,但我设计调度器之前,就尽可能降低调度器和电梯的耦合性,但是最后没能想出一个较好的办法,他们之间还是存在双向的信息传递。
二、bug分析
第一次第二次作业bug主要在于,输出方面的小瑕疵,尤其是第二次作业,在输出到达楼层和开关门之间,很容易有逻辑上的疏忽。
第三次作业bug主要在于换乘,换乘的是否合理,是否存在“电梯杀人情况”,是否存在“电梯过早停止的情况”,这是我遇到的bug中较为典型的
多线程bug单步调试已经很难进行,所以采取了输出调试的办法,这种办法也是比较高效的,较好的解决个各种bug。
三、互测策略
多线程程序会出现,bug存在但是很难复现的情况,所以找到一组比较好的数据,进行多次提交,可以较好的找到一些“强测幸存者”。
四、心得与体会
电梯单元让我清晰了解了java多线程的精妙,很明显的感觉到自己在一次一次作业中,逻辑不断完善,设计思路不断清晰。线程安全类的使用,物理锁与逻辑锁的使用,线程唤醒的细节,都是在一次又一次试错中慢慢摸索到的。用一个数据多次测试自己的程序,有时会出现bug,但是再复现起来可能要经过很多次的调试,这个过程是艰难的,但发现bug并成功解决收获的快乐和满足,也是很难用言语形容的。只有每次作业都全力以赴,才能感觉到自己在不断进步,以后的oo作业也会更努力的。